C++ 多线程编程入门:线程操作 + 四大同步机制

发布时间:2026/7/14 4:39:58
C++ 多线程编程入门:线程操作 + 四大同步机制 以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){ //线程中要做的事情 }首先函数返回值类型定义为DWORDDWORD表示双字一个字为两个字节。后面紧跟的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表示等待的最大时间以毫秒为单位。填INFINITE0xFFFFFFFF无限等待系统会一直卡住直到对象结束运行。填0立即返回相当于系统瞥了一眼对象结束了没。函数返回一个DWORD类型的变量。有下面几种返回值。WAIT_OBJECT_00x00000000成功触发说明等待线程已成功结束。WAIT_TIMEOUT0x00000102超时说明等待dwMilliseconds后对象仍未结束。WAIT_FAILED0xFFFFFFFF调用失败可能传入无效句柄。WAIT_ABANDONED0x00000080仅在对象为互斥量Mutex时可能触发表明上一个持有该互斥量的线程异常终止。3.两个例子#includeWindows.h #includecstdio 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()关闭句柄每次创建对象生成一个句柄都必须在使用结束后关闭它否则将导致内核对象无法被清除造成句柄泄露。类的静态成员函数作为线程函数。#includeWindows.h #includecstdio 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()销毁参数对应的临界区对象。#includeWindows.h #includecstdio 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。下面是一个示例代码显示甲乙两人各卖出的车票数。#includeWindows.h #includeiostream 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。使用事件对象实现和上一个示例一样的功能。#includeWindows.h #includeiostream 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个客人但超市场地有限一次只能接待三个客人每人每次只能买一件商品可以重复进入超市。#includeWindows.h #includeiostream 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、操作系统底层以及实际项目踩坑的内容欢迎常来一起交流一起成长 ——不想干活的十一