以Win32API函数为例介绍C++多线程编程。
一,线程的基本操作
1.创建线程
在Windows上创建线程需要使用WindowsAPI函数CreateThread(),其函数原型如下。
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, //安全属性 SIZE_T dwStackSize, //线程栈空间大小 LPTHREAD_START_ROUTINE lpStartAddress, //线程函数 LPVOID lpParameter, //线程函数参数 DWORD dwCreationFlags, //启动选项 LPDWORD lpThread //返回线程ID );第一个参数lpThreadAttributes表示线程的安全属性。决定该进程的子进程是否可以继承这个线程句柄,通常设置为NULL,表示使用默认安全性且句柄不可继承。
第二个参数dwStackSize表示线程栈的初始大小。一般取0值,表示使用系统默认大小。
第三个参数lpStartAddress表示线程函数指针。即新线程启动后要执行的函数。该函数有固定格式如下。
DWORD WINAPI ThreadProc(LPVOID lpParameter){ //线程中要做的事情 }首先,函数返回值类型定义为DWORD,DWORD表示双字(一个字为两个字节)。后面紧跟的WINAPI表示函数的调用约定,展开为__stdcall,这意味着函数参数从右向左入栈,且由被调用函数(即这个线程函数自己)清理堆栈。函数只有一个参数lpParameter,是一个无类型指针。
第四个参数lpParameter就是传递给线程函数的参数。
第五个参数dwCreationFlags是启动选项,有两个值可选,0表示线程建立后直接执行线程函数,CREATE_SUSPENDED表示线程建立后会挂起等待,以后使用ResumeThread()函数恢复线程运行,同样也可以使用SuspendThread(),函数挂起线程。
最后一个参数lpThreadId返回线程创建成功后的线程ID,这是线程的唯一标识,就像线程的身份证号,系统用它来区分不同线程。
返回值是一个HANDLE类型的变量,是一个句柄,句柄是一个用来唯一标识系统资源(如窗口、文件、图标等)的索引。它与ID识别身份的作用不同,它更像是门禁卡,用来实现对系统资源的操作和控制。
2.等待线程结束
有时有的线程想要结束必须等其它线程先结束,可以使用WaitForSingleObject()函数等待特定线程结束。其函数原型如下。
DWORD WINAPI WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);参数hHandle制定要等待的对象的句柄。
参数dwMilliseconds表示等待的最大时间,以毫秒为单位。填INFINITE(0xFFFFFFFF):无限等待,系统会一直卡住,直到对象结束运行。填0:立即返回,相当于系统瞥了一眼对象结束了没。
函数返回一个DWORD类型的变量。有下面几种返回值。
WAIT_OBJECT_0(0x00000000):成功触发,说明等待线程已成功结束。
WAIT_TIMEOUT(0x00000102):超时,说明等待dwMilliseconds后对象仍未结束。
WAIT_FAILED(0xFFFFFFFF):调用失败,可能传入无效句柄。
WAIT_ABANDONED(0x00000080):仅在对象为互斥量(Mutex)时可能触发,表明上一个持有该互斥量的线程异常终止。
3.两个例子
#include<Windows.h> #include<cstdio> DWORD WINAPI ThreadProc(LPVOID lpParameter) { //线程函数 for (int i = 0; i < 5; i++) { DWORD id = GetCurrentThreadId(); //获取目前线程的ID printf("ThreadProc %5d %d\n", id, i); for (int j = 0; j < 12000; j++) { //消耗很短的时间 ; } } return 0; } int main() { DWORD dwThreadID1, dwThreadID2; HANDLE hThread1 = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadID1); HANDLE hThread2 = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadID2); if (hThread1 == NULL || hThread2 == NULL) { printf("CreateThread failed\n"); } else { for (int i = 0; i < 5; i++) { printf("ThreadMain %d\n", i); for (int j = 0; j < 12000; j++) { ; } } WaitForSingleObject(hThread1, INFINITE); //等待线程结束 WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); //关闭线程句柄 CloseHandle(hThread2); } return 0; }运行结果如下。
ThreadMain 0 ThreadMain 1 ThreadProc 25476 0 ThreadProc 26544 0 ThreadMain 2 ThreadProc 25476 1 ThreadProc 25476 2 ThreadMain 3 ThreadProc 26544 1 ThreadProc 26544 2 ThreadMain 4 ThreadProc 25476 3 ThreadProc 25476 4 ThreadProc 26544 3 ThreadProc 26544 4说明:每次分配给线程的CPU时间是不定的,线程ID也是不确定的,所以每次程序运行结果是不同的。
注意“hThread1 == NULL”说明线程 创建失败返回NULL。最后先使用WaitForSingleObject()令主线程等待两个线程结束,再使用CloseHandle()关闭句柄,每次创建对象生成一个句柄都必须在使用结束后关闭它,否则将导致内核对象无法被清除造成句柄泄露。
类的静态成员函数作为线程函数。
#include<Windows.h> #include<cstdio> class MyThread { private: int amount; public: MyThread(int amount) { this->amount = amount; } static DWORD WINAPI ThreadProc(LPVOID lpParameter); }; DWORD WINAPI MyThread::ThreadProc(LPVOID lpParameter) { MyThread* pThread = (MyThread*)IpParameter; DWORD id = GetCurrentThreadId(); for (int i = 0; i < 5; i++) { printf("ThreadProc %5d %d\n", id, pThread->amount); pThread->amount--; for (int j = 0; j < 12000; j++) { ; } } return 0; } int main() { DWORD dwThreadID1, dwThreadID2; HANDLE hThread1, hThread2; MyThread myThread(100); hThread1 = CreateThread(NULL, 0, MyThread::ThreadProc, &myThread, 0, &dwThreadID1); hThread2 = CreateThread(NULL, 0, MyThread::ThreadProc, &myThread, 0, &dwThreadID2); if (hThread1 != NULL && hThread2 != NULL) { WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); } return 0; }由于类的非静态成员函数都含有隐含的参数this,而线程函数必须遵循特定的格式,所以只有类的静态函数可以作为线程函数。通常将当前对象的地址(this指针)传递给线程函数,以访问类中的成员。
运行结果如下。
ThreadProc 27412 100 ThreadProc 23472 100 ThreadProc 27412 99 ThreadProc 27412 97 ThreadProc 27412 96 ThreadProc 27412 95 ThreadProc 23472 98 ThreadProc 23472 93 ThreadProc 23472 92 ThreadProc 23472 91可以发现有的amount值出现两次而有的根本没出现,这是由于多个线程访问同一数据引起的。某个线程执行完打印步骤后,系统分配给它的CPU时间就用完了,并没有执行amount--,导致amount值重复,跳值。需要使用线程同步来解决这一问题。
二.线程同步
下面介绍四种线程同步的方法来解决上面例子里遇到的问题。
1.临界区对象
临界区(Critical Section)又称关键代码块。临界区的 代码块在某一时刻只允许一个线程执行,需要定义一个CRITICAL_SECTION类型的变量。
操作临界区的几个主要函数如下。
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection); void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection); void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection); void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);第一个函数IntializeCriticalSection()用来初始化一个临界区,参数LPCRITICAL_SECTION表示一个临界区对象的指针。
第二个函数EnterCriticalSection()表示临界区代码块的入口,当有线程试图访问临界区时,它先判断是否有线程正在访问临界区,如果没有则改变CRITICAL_SECTION结构体中的值,赋予当前线程访问权限,函数立即返回;如果有线程正在访问资源,则进入等待状态,直到该线程 访问结束。
第三个函数LeaveCriticalSection()释放参数对应的临界区资源,表示临界区的出口,位于这两个函数之间的代码就是临界区代码。
最后一个函数DeleteCriticalSection()销毁参数对应的临界区对象。
#include<Windows.h> #include<cstdio> class MyThread { private: int amount; CRITICAL_SECTION cs; //创建临界区对象cs public: MyThread(int amount) { this->amount = amount; InitializeCriticalSection(&cs); //初始化临界区对象cs } ~MyThread() { DeleteCriticalSection(&cs); //析构函数销毁临界区对象cs } static DWORD WINAPI ThreadProc(LPVOID lpParameter); }; DWORD WINAPI MyThread::ThreadProc(LPVOID lpParameter) { MyThread* pThread = (MyThread*)IpParameter; DWORD id = GetCurrentThreadId(); for (int i = 0; i < 5; i++) { EnterCriticalSection(&pThread->cs); //临界区代码块起始 printf("ThreadPro %5d %d\n", id, pThread->amount); pThread->amount--; LeaveCriticalSection(&pThread->cs); //临界区代码结束 for (int j = 0; j < 12000; j++) { ; } } return 0; } int main() { DWORD dwThreadID1, dwThreadID2; HANDLE hThread1, hThread2; MyThread myThread(100); hThread1 = CreateThread(NULL, 0, MyThread::ThreadProc, &myThread, 0, &dwThreadID1); hThread2 = CreateThread(NULL, 0, MyThread::ThreadProc, &myThread, 0, &dwThreadID2); if (hThread1 != NULL && hThread2 != NULL) { WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); } return 0; }运行结果如下。可以看到amount值没有重复了。
ThreadPro 25468 100 ThreadPro 25468 99 ThreadPro 25468 98 ThreadPro 4460 97 ThreadPro 4460 96 ThreadPro 4460 95 ThreadPro 4460 94 ThreadPro 25468 93 ThreadPro 4460 92 ThreadPro 25468 912.互斥对象
互斥对象也称互斥量,只有拥有互斥对象的线程才拥有访问共享资源的权限。使用互斥对象通常需要等待函数WaitForSingleObject()的配合。当没有线程拥有互斥对象时,系统将互斥对象设置为有信号量状态,向外发送信号,如果此时有线程在等待该互斥对象,则该线程可以获得该互斥对象,并将该互斥对象设置为无信号状态,不向外发送信号。当线程访问完共享资源后,可以调用ReleaseMutex()函数释放互斥对象的所有权,重新将互斥对象设置为有信号状态。
操作互斥对象的函数主要有下面两个。
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCWSTR lpName); BOOL ReleaseMutex(HANDLE hMutex);第一个函数CreateMutex()用于创建互斥对象。第一个参数表示安全属性,一般使用NULL表示默认属性;第二个参数制定初始化互斥对象的所有者,如果是TRUE表示创建该互斥对象的线程立刻拥有该互斥对象的所有权,如果是FALSE则不拥有;参数lpName是一个字符串指针,表示该互斥对象的名字,如果为NULL则创建一个匿名互斥量,仅能在同一进程间的的线程中使用,否则创建一个命名互斥量,全局可见,可用于跨进程同步。返回创建的互斥对象的句柄。
第二个函数用于释放互斥对象的所有权。成功返回非0,失败返回0;
回到前面函数WaitForSingleObject()的返回值问题,返回参数WAIT_ABANDONED就表示如果持有该互斥量的进程已经结束,但未调用ReleaseMutex()释放所有权,就会返回WAIT_ABANDONED。
下面是一个示例代码,显示甲乙两人各卖出的车票数。
#include<Windows.h> #include<iostream> using namespace std; DWORD WINAPI FunProc(LPVOID lpParameter); //线程函数 int g_tickets = 10; //剩余票数 HANDLE g_hMutex; //互斥对象句柄 int main() { HANDLE hThread1, hThread2; g_hMutex = CreateMutex(NULL, FALSE, NULL); //创建互斥对象 if (!g_hMutex) { cout << "Mutex 创建失败!!!" << endl; return 0; } hThread1 = CreateThread(NULL, 0, FunProc, (LPVOID)0, 0, NULL); hThread2 = CreateThread(NULL, 0, FunProc, (LPVOID)1, 0, NULL); if (hThread1 == NULL || hThread2 == NULL) { cout << "线程创建失败!!!" << endl; return 0; } WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); CloseHandle(g_hMutex); //关闭互斥对象句柄 return 0; } DWORD WINAPI FunProc(LPVOID lpParameter) { while (TRUE) { WaitForSingleObject(g_hMutex, INFINITE); //等待互斥对象有信号 if (g_tickets > 0) { --g_tickets; if (lpParameter == 0) { cout << "甲卖出一张车票,还剩车票:" << g_tickets << endl; } else { cout << "乙卖出一张车票,还剩车票:" << g_tickets << endl; } ReleaseMutex(g_hMutex); //释放互斥对象所有权 } else { ReleaseMutex(g_hMutex); break; } } return 0; }运行结果如下。
甲卖出一张车票,还剩车票:9 乙卖出一张车票,还剩车票:8 甲卖出一张车票,还剩车票:7 乙卖出一张车票,还剩车票:6 甲卖出一张车票,还剩车票:5 乙卖出一张车票,还剩车票:4 甲卖出一张车票,还剩车票:3 乙卖出一张车票,还剩车票:2 甲卖出一张车票,还剩车票:1 乙卖出一张车票,还剩车票:03.事件对象
事件对象(Event)的使用方法与互斥对象类似,但功能更多一些。事件对象的主要操作函数如下。
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCWSTR lpName ); BOOL SetEvent(HANDLE hEvent); BOOL ResetEvent(HANDLE hEvent);第一个函数用于创建事件对象。第一参数lpEventAttributes表示安全属性;第二参数bManualReset表示重置模式,设置为TRUE时为手动重置,每次都必须使用ResetEvent()函数将事件恢复到无信号状态,设置为FALSE,那么当一个线程得到事件对象后,系统自动将事件复原为无信号状态;第三个参数bInitialState指定事件对象的初始状态,设置为TRUE则事件对象创建后就处于有信号状态,FALSE则处于无信号状态。最后一个参数lpName指定事件对象的名称。函数返回创建的事件对象的句柄。
第二个函数将设置对应的事件对象为有信号状态。第三个函数则设置为无信号状态。都是成功返回非0,失败返回0。
使用事件对象实现和上一个示例一样的功能。
#include<Windows.h> #include<iostream> using namespace std; DWORD WINAPI FunProc(LPVOID lpParameter); //线程函数 int g_tickets = 10; //剩余票数 HANDLE g_hEvent; //事件对象句柄 int main() { HANDLE hThread1; HANDLE hThread2; g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); //创建事件对象 if (!g_hEvent) { cout << "Event创建失败!!!" << endl; return 0; } hThread1 = CreateThread(NULL, 0, FunProc, (LPVOID)0, 0, NULL); hThread2 = CreateThread(NULL, 0, FunProc, (LPVOID)1, 0, NULL); if (hThread1 == NULL || hThread2 == NULL) { cout << "线程创建失败!!!" << endl; return 0; } WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); CloseHandle(g_hEvent); //关闭事件对象句柄 return 0; } DWORD WINAPI FunProc(LPVOID lpParameter) { while (TRUE) { WaitForSingleObject(g_hEvent, INFINITE); //等待事件对象信号 if (g_tickets > 0) { --g_tickets; if (lpParameter == 0) { cout << "甲卖出一张车票,还剩车票:" << g_tickets << endl; } else { cout << "乙卖出一张车票,还剩车票:" << g_tickets << endl; } SetEvent(g_hEvent); //使事件对象处于有信号状态 } else { SetEvent(g_hEvent); break; } } return 0; }运行结果如下。
乙卖出一张车票,还剩车票:9 甲卖出一张车票,还剩车票:8 乙卖出一张车票,还剩车票:7 甲卖出一张车票,还剩车票:6 乙卖出一张车票,还剩车票:5 甲卖出一张车票,还剩车票:4 乙卖出一张车票,还剩车票:3 甲卖出一张车票,还剩车票:2 乙卖出一张车票,还剩车票:1 甲卖出一张车票,还剩车票:04.信号量对象
信号量(Semaphore)内部有一个计数器,当计数器大于0时,信号量处于有信号状态,此时如有线程访问则允许访问并将计数器减1。线程离开后再加1。计数器为0时则为无信号状态,此时线程不能访问。
操作信号量的主要函数如下。
HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCWSTR lpName ); BOOL ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount );第一个函数创建一个信号量。第一个参数lpSemaphoreAttributes表示安全属性;第二个参数lInitialCount指定信号量计数器的初始值;第三个参数lMaximumCount指定信号量计数器的最大值;最后一个参数为信号量定义一个名称。函数返回信号量的句柄。
第二个函数用于增加信号量的计数。第一个参数hSemaphore表示操作信号量的句柄;第二个参数lReleaseCount指定要增加计数的个数;最后一个参数lpPrevoiusCount用于获取增加前的个数。
下面这个示例为:总共10件商品,现有4个客人,但超市场地有限,一次只能接待三个客人,每人每次只能买一件商品,可以重复进入超市。
#include<Windows.h> #include<iostream> using namespace std; DWORD WINAPI FunProc(LPVOID lpParameter); HANDLE g_hSemaphore; CRITICAL_SECTION g_cs; int Quantity = 10; int main() { InitializeCriticalSection(&g_cs); g_hSemaphore = CreateSemaphore(NULL, 3, 3, NULL); //创建信号量对象 if (!g_hSemaphore) { cout << "Semaphore 创建失败!!!" << endl; return 0; } HANDLE hThread[4]; //创建4个线程,表示4个客人 for (int i = 0; i < 4; i++) { hThread[i] = CreateThread(NULL, 0, FunProc, NULL, 0, NULL); } for (int i = 0; i < 4; i++) { if (hThread[i] != NULL) { WaitForSingleObject(hThread[i], INFINITE); CloseHandle(hThread[i]); } } CloseHandle(g_hSemaphore); //关闭信号量句柄 DeleteCriticalSection(&g_cs); return 0; } DWORD WINAPI FunProc(LPVOID lpParameter) { DWORD dwThreadID = GetCurrentThreadId(); while (true) { if (WaitForSingleObject(g_hSemaphore, INFINITE) == WAIT_OBJECT_0) { EnterCriticalSection(&g_cs); //允许三人进如,一次接待一人 if (Quantity > 0) { --Quantity; cout << "线程:" << dwThreadID << " 购买一件, 还剩:" << Quantity << endl; LeaveCriticalSection(&g_cs); ReleaseSemaphore(g_hSemaphore, 1, NULL); //资源计数加1 } else { cout << "商品已经售完, 线程:" << dwThreadID << "结束."; long n; //记录先前计数器的值 ReleaseSemaphore(g_hSemaphore, 1, &n); cout << " 之前信号计数量:" << n << endl; LeaveCriticalSection(&g_cs); break; } } } return 0; }结果如下。
线程:10500 购买一件, 还剩:9 线程:1440 购买一件, 还剩:8 线程:4272 购买一件, 还剩:7 线程:1440 购买一件, 还剩:6 线程:4272 购买一件, 还剩:5 线程:18076 购买一件, 还剩:4 线程:10500 购买一件, 还剩:3 线程:18076 购买一件, 还剩:2 线程:4272 购买一件, 还剩:1 线程:18076 购买一件, 还剩:0 商品已经售完, 线程:10500结束. 之前信号计数量:0 商品已经售完, 线程:1440结束. 之前信号计数量:0 商品已经售完, 线程:4272结束. 之前信号计数量:1 商品已经售完, 线程:18076结束. 之前信号计数量:2写在最后
非常感谢您能耐心读完这篇关于 Win32 多线程编程的入门分享。如果文中有任何表述不准确、代码不够优雅或逻辑有误之处,恳请各位大佬在评论区不吝指正,我一定虚心学习、及时修正。
如果您觉得这篇文章对您有所帮助,不妨点个赞 👍 或关注我,您的每一次鼓励都是我继续输出的动力。后续我还会分享更多关于 C++、操作系统底层以及实际项目踩坑的内容,欢迎常来,一起交流,一起成长! 🚀
——不想干活的十一