c++多线程同步机制

前序文章:一文搞定c++多线程

同步与互斥

现代操作系统都是多任务操作系统,通常同一时刻有大量可执行实体,则运行着的大量任务可能需要访问或使用同一资源,或者说这些任务之间具有依赖性。

  • 线程同步:线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。例如:两个线程A和B在运行过程中协同步调,按预定的先后次序运行,比如 A 任务的运行依赖于 B 任务产生的数据。
  • 线程互斥:线程互斥是指对于共享的操作系统资源,在各线程访问时具有排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许有限的线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。例如:两个线程A和B在运行过程中共享同一变量,但为了保持变量的一致性,如果A占有了该资源则B需要等待A释放才行,如果B占有了该资源需要等待B释放才行。

为什么需要线程同步

​ 由于现在操作系统支持多个线程运行,可能多个线程之间会共享同一资源。当多个线程去访问同一资源时,如果不加以干预,可能会引起冲突。例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。

​ 举个例子,现在你银行卡里有100元,然后一个线程去执行消费,一个线程去执行充值,如果不加以干预,则可能出现这样的情况:消费的线程读取到你的卡里有100元,然后由于线程切换保存了当前的状态就去执行充值线程,充值线程完成充值后你的卡里实际上应该是10000元,然后切换到消费进程,消费进程由于已经读取过卡里的钱所以会直接进行之后的操作,完成后计算得到卡里的钱应该改为50,这便会将你真实的卡里的钱改成50,这当然是我们不希望看到的!如果进行了线程同步操作,当消费线程进行时,由于这是对数据进行写的操作,那么其他充值线程都需要被阻塞直至消费进程结束占据资源,这样便不会导致数据的不一致。

​ 举个代码例子,两个线程对一个共享数据进行++操作并且输出出来,代码如下:

#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
int share = 0;  //共享变量
void thread1()
{while(share<20){share++;cout << "this is thread1! share is " << share << endl;Sleep(100);}
}
void thread2()
{while (share < 20){share++;cout << "this is thread2! share is " << share << endl;Sleep(100);}
}int main()
{thread task1(thread1); thread task2(thread2); task1.join();task2.join();cout << "main thread!" << endl;
}

某一次的运行结果:

PS D:\vscode_c> ./test
this is thread1! share is 2
this is thread2! share is 2
this is thread1! share is 3
this is thread2! share is 4
this is thread1! share is this is thread2! share is 6
6
this is thread2! share is 8
this is thread1! share is 8
this is thread2! share is 9
this is thread1! share is 10
this is thread1! share is 12
this is thread2! share is 13
this is thread1! share is 14
this is thread2! share is 15
this is thread1! share is 16
this is thread2! share is this is thread1! share is 18
18
this is thread1! share is 20
this is thread2! share is 20
main thread!

可以看到,不但出现两个线程读取的变量值一样的现象(我们当然期望的是每一行都是一个唯一的数字并且有一个换行),还出现了cout的内容包括数字和换行符位置也有些错乱。主要原因是share和cout的缓冲区是thread1和thread2共享的,由于两个线程同时运行,便可能将一个已经修改的值读取,或者将另一个线程已经读取但是未修改的值进行读取,还有可能将另一个线程已经放入缓冲区的内容输出。当然,这个输出是不符合我们预期的!

mutex互斥锁

互斥锁是一种简单的通过加锁的方式控制多个线程对共享资源的访问,互斥锁有两个状态,上锁与解锁(lock和unlock)。lock互斥锁是一个原子操作,这说明在同一时刻只能有一个线程锁住互斥锁,不会出现同时上锁的情况,同时,互斥锁具有唯一性,一旦上锁,其他线程不能够再将其锁住。当一个互斥锁被锁住时,其他希望锁住该锁的线程将被挂起,直至该互斥锁被unlock解开,则这些线程将被唤醒并其中一个将再次抢占成功。

互斥锁的通常执行流程如下:

  • 在访问共享资源的临界区前,将互斥锁锁住 lock
  • 在完成访问共享资源的操作后,将互斥锁unlock
  • 这期间,其他线程如果需要访问共享资源将调用lock,将自身挂起,直至该互斥锁被unlock才行

利用互斥锁修改上面的代码:

#include <iostream>
#include <thread>
#include <Windows.h>
#include<mutex>
using namespace std;
mutex mut;
int share = 0;
void thread1()
{while(share<20){mut.lock();   //将互斥锁进行lockshare++;cout << "this is thread1! share is " << share << endl;Sleep(100);mut.unlock();  //unlock 解开互斥锁}
}
void thread2()
{while (share < 20){mut.lock();   //将互斥锁进行lockshare++;cout << "this is thread2! share is " << share << endl;Sleep(100);mut.unlock();  //unlock 解开互斥锁}
}int main()
{thread task1(thread1); thread task2(thread2); task1.join();task2.join();cout << "main thread!" << endl;
}

查看运行结果:

PS D:\vscode_c> ./test
this is thread1! share is 1
this is thread2! share is 2
this is thread1! share is 3
this is thread2! share is 4
this is thread2! share is 5
this is thread2! share is 6
this is thread2! share is 7
this is thread2! share is 8
this is thread2! share is 9
this is thread1! share is 10
this is thread2! share is 11
this is thread1! share is 12
this is thread1! share is 13
this is thread1! share is 14
this is thread2! share is 15
this is thread2! share is 16
this is thread2! share is 17
this is thread1! share is 18
this is thread1! share is 19
this is thread2! share is 20
main thread!

很显然,这是符合我们预期的。不过事情总要做到更好,这个方法有什么问题呢?试想一下,如果在lock和unlock之间发生了异常,则可能永远不会执行到unlock,另一个进程将永远被挂起在那里等待。

为了解决该问题,根据对象的析构函数自动调用的原理,c++11推出了std::lock_guard自动释放锁,其原理是:声明一个局部的lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:在定义该局部对象的时候加锁(调用构造函数),出了该对象作用域的时候解锁(调用析构函数)。

在C++中,通过构造std::mutex的实例来创建互斥元,可通过调用其成员函数lock()和unlock()来实现加锁和解锁,然后这是不推荐的做法,因为这要求程序员在离开函数的每条代码路径上都调用unlock(),包括由于异常所导致的在内。作为替代,标准库提供了std::lock_guard类模板,实现了互斥元的RAII惯用语法(资源获取即初始化)。该对象在构造时锁定所给的互斥元,析构时解锁该互斥元,从而保证被锁定的互斥元始终被正确解锁

#include <iostream>
#include <thread>
#include <Windows.h>
#include<mutex>
using namespace std;
mutex mut;
int share = 0;
void thread1()
{while(share<20){std::lock_guard<std::mutex> mtx_locker(mut);  //用lock_guard实现互斥锁if(share>=20)break;share++;cout << "this is thread1! share is " << share << endl;Sleep(100);}
}
void thread2()
{while (share < 20){std::lock_guard<std::mutex> mtx_locker(mut);  //用lock_guard实现互斥锁if (share >= 20)break;share++;cout << "this is thread2! share is " << share << endl;Sleep(100);}
}int main()
{thread task1(thread1); thread task2(thread2); task1.join();task2.join();cout << "main thread!" << endl;
}

运行结果:

PS D:\vscode_c> ./test
this is thread1! share is 1
this is thread2! share is 2
this is thread2! share is 3
this is thread2! share is 4
this is thread2! share is 5
this is thread2! share is 6
this is thread2! share is 7
this is thread2! share is 8
this is thread2! share is 9
this is thread2! share is 10
this is thread2! share is 11
this is thread1! share is 12
this is thread2! share is 13
this is thread2! share is 14
this is thread2! share is 15
this is thread2! share is 16
this is thread1! share is 17
this is thread2! share is 18
this is thread2! share is 19
this is thread1! share is 20
main thread!

win32的四种同步方式

临界区

临界区 (Critical Section) 是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用 EnterCriticalSection()和LeaveCriticalSection() 函数去标识和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection() 的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。

临界区的使用:

  • 定义一个CRITICAL_SECTION类型的变量

  • 调用InitializeCriticalSection函数对变量进行初始化,函数的作用是初始化临界区,唯一的参数是指向结构体CRITICAL_SECTION的指针变量

    VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection )
    
  • 为了将某段代码设置为临界区,在进入这段代码前调用EnterCriticalSection函数。该函数的作用是使调用该函数的线程进入已经初始化的临界区,并拥有该临界区的所有权。这是一个阻塞函数,如果线程获得临界区的所有权成功,则该函数将返回,调用线程继续执行,否则该函数将一直等待,这样会造成该函数的调用线程也一直等待。如果不想让调用线程等待(非阻塞),则应该使用TryEnterCriticalSection函数

    VOID WINAPI EnterCriticalSection(__inout LPCRITICAL_SECTION lpCriticalSection);
    
  • 在临界区代码后,需要调用LeaveCriticalSection函数。该函数的作用是使调用该函数的线程离开临界区并释放对该临界区的所有权,以便让其他线程也获得访问该共享资源的机会

    void WINAPI LeaveCriticalSection( _Inout_LPCRITICAL_SECTION lpCriticalSection);
    

    如果一个线程在进入临界区后没有调用LeaveCriticalSection,则会出现等待进入临界区的线程无限期等待的问题

  • 最后释放掉CRITICAL_SECTION结构指针,该函数的作用是删除程序中已经被初始化的临界区。如果函数调用成功,则程序会将内存中的临界区删除,防止出现内存错误。

    void WINAPI DeleteCriticalSection(_Inout_ LPCRITICAL_SECTION lpCriticalSection);
    

tips:单进程的线程可以使用临界资源对象来解决同步互斥问题,该对象不能保证哪个线程能够获得到临界资源对象,因而该系统能公平的对待每一个线程

利用临界区解决上面的问题:

#include <iostream>
#include <thread>
#include <windows.h>
#include<mutex>
using namespace std;
CRITICAL_SECTION Critical; //定义临界区句柄
int share = 0;
void thread1()
{while(share<20){EnterCriticalSection(&Critical);if(share>=20)break;share++;cout << "this is thread1! share is " << share << endl;Sleep(100);LeaveCriticalSection(&Critical);}
}
void thread2()
{while (share < 20){EnterCriticalSection(&Critical);if (share >= 20)break;share++;cout << "this is thread2! share is " << share << endl;Sleep(100);LeaveCriticalSection(&Critical);}
}int main()
{InitializeCriticalSection(&Critical); //初始化临界区对象thread task1(thread1); thread task2(thread2); task1.join();task2.join();cout << "main thread!" << endl;
}

写起来和mutex类似,主要注意 的是一定要先初始化临界区对象。

运行结果:

PS D:\vscode_c> ./test
this is thread1! share is 1
this is thread1! share is 2
this is thread1! share is 3
this is thread1! share is 4
this is thread1! share is 5
this is thread1! share is 6
this is thread2! share is 7
this is thread2! share is 8
this is thread2! share is 9
this is thread1! share is 10
this is thread1! share is 11
this is thread1! share is 12
this is thread1! share is 13
this is thread1! share is 14
this is thread1! share is 15
this is thread1! share is 16
this is thread1! share is 17
this is thread1! share is 18
this is thread1! share is 19
this is thread1! share is 20
main thread!
事件

事件(Event)是WIN32提供的最灵活的线程间同步方式,事件可以处于激发状态或未激发状态。应用时,通过使用 CreateEvent 函数创建事件,然后使用信号控制线程运行。其中将事件变为有信号可使用 SetEvent 函数,将事件信号复位(变为无信号)可使用 ResetEvent 函数,信号可以配合 WaitForSingleObject 函数对线程的同步进行控制,当有信号时,此函数便会放行;无信号时,此函数会将阻塞。

根据状态变迁方式的不同,事件可分为两类:
(1)手动设置:这种对象只可能用程序手动设置,在需要该事件或者事件发生时,采用SetEvent及ResetEvent来进行设置。
(2)自动恢复:一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再次设置。

相关的函数:

函数名 函数说明
CreateEvent Creates or opens a named or unnamed event object.
CreateEventEx Creates or opens a named or unnamed event object and returns a handle to the object.
OpenEvent Opens an existing named event object.
PulseEvent Sets the specified event object to the signaled state and then resets it to the nonsignaled state after releasing the appropriate number of waiting threads.
ResetEvent Sets the specified event object to the nonsignaled state.
SetEvent Sets the specified event object to the signaled state.

CreateEvent用于创建事件对象,函数原型为:

HANDLE WINAPI CreateEvent(_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,_In_     BOOL                  bManualReset,_In_     BOOL                  bInitialState,  _In_opt_ LPCTSTR               lpName
);

着重强调一下第二个参数,CreateEvent的第二个参数 bManualReset 表示指定将事件对象创建成手动复原还是自动复原,如果是TRUE,那么必须用ResetEvent函数来手工将事件的状态复原到无信号状态。如果设置为FALSE,当事件被一个等待线程释放以后,系统将会自动将事件状态复原为无信号状态。第三个参数bInitialState 表示事件对象的初始状态。如果为true,则表示该事件对象初始时为有信号状态

利用事件解决上面的问题:

#include <iostream>
#include <thread>
#include <windows.h>
#include<mutex>
using namespace std;
HANDLE hEvent; //定义事件句柄
int share = 0;
void thread1()
{while(share<20){WaitForSingleObject(hEvent, INFINITE); //等待对象为有信号状态if(share>=20)break;share++;cout << "this is thread1! share is " << share << endl;Sleep(100);SetEvent(hEvent);  //将事件设置为有信号状态}
}
void thread2()
{while (share < 20){WaitForSingleObject(hEvent, INFINITE); //等待对象为有信号状态if (share >= 20)break;share++;cout << "this is thread2! share is " << share << endl;Sleep(100);SetEvent(hEvent);  //将事件设置为有信号状态}
}int main()
{hEvent = CreateEvent(NULL, FALSE, TRUE, "event");  //创建事件   是自动恢复状态thread task1(thread1); thread task2(thread2); task1.join();task2.join();cout << "main thread!" << endl;
}

运行结果:

PS D:\vscode_c> ./test
this is thread1! share is 1
this is thread2! share is 2
this is thread1! share is 3
this is thread2! share is 4
this is thread1! share is 5
this is thread2! share is 6
this is thread1! share is 7
this is thread2! share is 8
this is thread1! share is 9
this is thread2! share is 10
this is thread1! share is 11
this is thread2! share is 12
this is thread1! share is 13
this is thread2! share is 14
this is thread1! share is 15
this is thread2! share is 16
this is thread1! share is 17
this is thread2! share is 18
this is thread1! share is 19
this is thread2! share is 20
main thread!
信号量

信号量是维护0到指定最大值之间的同步对象,用于线程的同步或者限制线程运行的数量。信号量状态在其计数大于0时是有信号的,而其计数是0时是无信号的。信号量对象在控制上可以支持有限数量共享资源的访问。

通常来说,信号量具有如下特点:

  • 如果当前资源的数量大于0,则信号量有效
  • 如果当前资源数量是0,则信号量无效
  • 当前资源的数量不能够为负值
  • 当前资源数量一定小于等于最大资源数量

信号量相关的函数:

//头文件
#include <windows.h>//创建信号量API
HANDLE WINAPI CreateSemaphore(_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,//指向SECURITY_ATTRIBUTES的指针;_In_     LONG                  lInitialCount,          //信号量对象的初始值;_In_     LONG                  lMaximumCount,  //信号量对象的最大值,这个值必须大于0;_In_opt_ LPCTSTR               lpName                 //信号量对象的名称;
);//等待信号量API
DWORD WINAPI WaitForSingleObject(_In_ HANDLE hHandle,          //信号量对象句柄_In_ DWORD  dwMilliseconds    //等待信号量时间,INFINET代表永久等待;
);//打开信号量
HANDLE OpenSemaphore (DWORD fdwAccess,      //accessBOOL bInherithandle,  //如果允许子进程继承句柄,则设为TRUEPCTSTR pszName  //指定要打开的对象的名字);//释放信号量句柄
BOOL WINAPI ReleaseSemaphore(_In_      HANDLE hSemaphore,         //信号量对象句柄;_In_      LONG   lReleaseCount,      //信号量释放的值,必须大于0;_Out_opt_ LPLONG lpPreviousCount     //前一次信号量值的指针,不需要可置为空;
);

用信号量解决上面的问题:

#include <iostream>
#include <thread>
#include <windows.h>
#include<mutex>
using namespace std;
HANDLE hSemaphore; //定义信号量句柄
int share = 0;
void thread1()
{while(share<20){WaitForSingleObject(hSemaphore, INFINITE); //等待信号量为有信号状态if(share>=20)break;share++;cout << "this is thread1! share is " << share << endl;Sleep(100);ReleaseSemaphore(hSemaphore, 1, nullptr);  //释放信号量}
}
void thread2()
{while (share < 20){WaitForSingleObject(hSemaphore, INFINITE); //等待信号量为有信号状态if (share >= 20)break;share++;cout << "this is thread2! share is " << share << endl;Sleep(100);ReleaseSemaphore(hSemaphore, 1, nullptr); //释放信号量}
}int main()
{hSemaphore = CreateSemaphore(NULL, 1, 20, "semaphore"); //创建信号量thread task1(thread1); thread task2(thread2); task1.join();task2.join();cout << "main thread!" << endl;
}

运行结果:

PS D:\vscode_c> ./test
this is thread1! share is 1
this is thread2! share is 2
this is thread1! share is 3
this is thread2! share is 4
this is thread1! share is 5
this is thread2! share is 6
this is thread1! share is 7
this is thread2! share is 8
this is thread1! share is 9
this is thread2! share is 10
this is thread1! share is 11
this is thread2! share is 12
this is thread1! share is 13
this is thread2! share is 14
this is thread1! share is 15
this is thread2! share is 16
this is thread1! share is 17
this is thread2! share is 18
this is thread1! share is 19
this is thread2! share is 20
main thread!
互斥量

windows下提供有互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。不过该互斥量和mutex基本一样,所以用移植性更好的mutex更好

互斥量主要的函数:

//创建互斥量
HANDLE WINAPI CreateMutex(__in          LPSECURITY_ATTRIBUTES lpMutexAttributes,//互斥对象的安全属性__in          BOOL bInitialOwner,//互斥对象的初始状态;TRUE表示互斥对象的线程ID为当前调度线程的线程ID,当前创建互斥对象的线程具有他的拥有权,互斥对象的递归计数器为1__in          LPCTSTR lpName//互斥对象的名称,NULL表示创建一个匿名的互斥对象
);
//释放互斥量
BOOL WINAPI ReleaseMutex(__in          HANDLE hMutex
);
//等待互斥量
DWORD WINAPI WaitForSingleObject(__in          HANDLE hHandle,//等待内核对象句柄__in          DWORD dwMilliseconds//等待时间,INFINITE表示无限等待
);

用互斥量解决上面的问题:

#include <iostream>
#include <thread>
#include <windows.h>
#include<mutex>
using namespace std;
HANDLE hMutex; //定义互斥对象句柄
int share = 0;
void thread1()
{while(share<20){WaitForSingleObject(hMutex, INFINITE);  //等待互斥量if(share>=20)break;share++;cout << "this is thread1! share is " << share << endl;Sleep(100);ReleaseMutex(hMutex);  //释放互斥量}
}
void thread2()
{while (share < 20){WaitForSingleObject(hMutex, INFINITE); //等待互斥量if (share >= 20)break;share++;cout << "this is thread2! share is " << share << endl;Sleep(100);ReleaseMutex(hMutex); //释放互斥量}
}int main()
{hMutex = CreateMutex(NULL, false, "mutex"); //创建互斥对象thread task1(thread1); thread task2(thread2); task1.join();task2.join();cout << "main thread!" << endl;
}

运行结果:

PS D:\vscode_c> ./test
this is thread1! share is 1
this is thread2! share is 2
this is thread1! share is 3
this is thread2! share is 4
this is thread1! share is 5
this is thread2! share is 6
this is thread1! share is 7
this is thread2! share is 8
this is thread1! share is 9
this is thread2! share is 10
this is thread1! share is 11
this is thread2! share is 12
this is thread1! share is 13
this is thread2! share is 14
this is thread1! share is 15
this is thread2! share is 16
this is thread1! share is 17
this is thread2! share is 18
this is thread1! share is 19
this is thread2! share is 20
main thread!

一文搞定c++多线程同步机制相关推荐

  1. 一文搞定c++多线程

    一文搞定c++多线程 c++11引入了用于多线程操作的thread类,该库移植性更高,并且使得写多线程变得简洁了一些. 多线程头文件支持 为了支持多线程操作,c++11新标准引入了一些头文件来支持多线 ...

  2. 一文搞定 Spring Security 异常处理机制!

    今天来和小伙伴们聊一聊 Spring Security 中的异常处理机制. 在 Spring Security 的过滤器链中,ExceptionTranslationFilter 过滤器专门用来处理异 ...

  3. php带参数单元测试_一文搞定单元测试核心概念

    基础概念 单元测试(unittesting),是指对软件中的最小可测试单元进行检查和验证,这里的最小可测试单元通常是指函数或者类.单元测试是即所谓的白盒测试,一般由开发人员负责测试,因为开发人员知道被 ...

  4. 【Java多线程】轻松搞定Java多线程(一)

    轻松搞定Java多线程(一) Java多线程详解(一) 1. 线程简介 2.线程的创建 2.1 三种创建方式 2.2 Thread 2.3 实现Runnable 2.3.1 初识并发问题 2.3.2 ...

  5. 【嵌入式开发-AD19】六文搞定Altium Designer-第一章:AD介绍及原理图库的创建

    [嵌入式开发-AD19]六文搞定Altium Designer-第一章:AD介绍及原理图库的创建 在文章的开头我想首先简单介绍一下国产全免费EDA软件,嘉立创EDA.嘉立创EDA拥有网页版和安装版两种 ...

  6. 四、通勤路上搞定 Java 多线程面试(1)

    前言 谈到多线程,一般都会联想到高并发,但是实际上两者并不是一个概念,高并发一般指的是从业务方面的描述系统的并发负载能力,而多线程只不过是如何使CPU的利用率达到最大化.因此一般问到高并发,都会从你的 ...

  7. 最强绘图AI:一文搞定Midjourney(附送咒语)

    最强绘图AI:一文搞定Midjourney(附送咒语) Midjourney官网:https://www.midjourney.com 简介 Midjourney是目前效果最棒的AI绘图工具.访问Mi ...

  8. 一文搞懂 Python 的 import 机制

    一.前言 希望能够让读者一文搞懂 Python 的 import 机制 1.什么是 import 机制? 通常来讲,在一段 Python 代码中去执行引用另一个模块中的代码,就需要使用 Python ...

  9. 【Python基础】一文搞定pandas的数据合并

    作者:来源于读者投稿 出品:Python数据之道 一文搞定pandas的数据合并 在实际处理数据业务需求中,我们经常会遇到这样的需求:将多个表连接起来再进行数据的处理和分析,类似SQL中的连接查询功能 ...

最新文章

  1. 基于kryo序列化方案的memcached-session-manager多memcached...
  2. 新手一看就懂的线程池
  3. build-android-in-OS-X-Yosemite-Xcode-7
  4. c#数据结构之集合的实现(数组及链表两种实现)
  5. 网络爬虫--之爬起校招信息代码
  6. dubbo源码解析-集群容错架构设计
  7. python正则表达式指南_Python正则表达式指南(转)
  8. mysql远程访问时间长无反应_远程MySQL访问需要很长时间
  9. poj 3469(网络流模版)
  10. 样本分布不平衡,机器学习准确率高又有什么用?
  11. url传递中文的解决方案总结
  12. 2020网上答题拿证书的竞赛_参赛答题拿证书—全国大学生知识竞赛
  13. kubernetes视频教程笔记 (25)-集群调度-调度过程说明
  14. java jsonobject 清空_有没有办法,我可以清空整个JSONObject – java
  15. 计算机病毒怎么侵入nide计算机,处理被病毒侵入电脑正确的方法图文教程
  16. 记一次简单的burpsuite弱口令爆破实验
  17. filtering_audio.c/filtering_video.c 解读
  18. 使用cef3开发的浏览器不支持flash问题的解决
  19. elasticsearch-analysis-ik中文分词插件安装及配置Ik自定义词典+拼音分词
  20. 为什么使用kbhit后按下键盘无反应?

热门文章

  1. exit(0),ExitProcess,和TerminateProcess的区别和联系
  2. 魅族手机可以装鸿蒙系统吗,魅族加入,中国手机或鼎力支持,华为鸿蒙挑战谷歌安卓有希望...
  3. DataSet与Iterator用法总结
  4. python爬虫系列—— requests和BeautifulSoup库的基本用法
  5. 全国计算机技术与软件专业技术资格(水平)考试,全国计算机技术与软件专业技术资格(水平)考试 2...
  6. java学生管理系统代码_java学生信息管理系统(附源码)
  7. python3.7安装
  8. sendto python_Python sendto似乎没有发送
  9. python内存管理 变量无需指定类型_Python内存管理
  10. 管理类联考——英语二——技巧篇——写作——图表作文——万能句