2023年6月22日发(作者:)
VC++中多线程学习(MFC多线程)三(线程同步包含:原⼦互锁、关键代码段、互斥器Mute。。。⽬录线程同步的必要性:我们知道操作系统的执⾏最⼩单位是线程,⽽⼀个进程包含了很多的线程,现在已经实现了真正的并⾏,如双核cpu,在每个核⼼⾥开⼀个进程,则双核cpu就可以开两个并⾏运⾏的进程⽽在每个进程类⼜可以开很多的线程,这⾥需要强调的是在两个核跑的两个进程是实实在在的并⾏的, 不会互相⼲扰,但是在每个核的进程中⼜运⾏了很多的线程,⽽这些线程并不是并⾏的,⽽是串⾏的,即操作系统会在线程中不停的切换执⾏,在切换执⾏很快时,给我们的感觉像是并⾏,但是实际并不是并⾏,这⾥⼤家需要注意,因此在同⼀个进程中的线程执⾏是串⾏的,这个⼤家需要理解。在⼀个进程中,我们定义⼀个全局变量,此时有很多的线程都在调⽤或者修改这个全局变量,那么会不会存在这样的⼀个情况,就是其中⼀个进程刚修改了这个全局变量,还没来得及使⽤就被切换到其他线程去了,⽽被切换的这个线程刚好也要修改或者使⽤这个全局变量,那个此时会不会出现问题呢?但是肯定的,会导致⽆法预估的错误,这也就是线程在操作这个变量时需要遵守⼀定的规则的重要性的原因,⽽线程同步就是解决这样类似问题的⽅法,因此线程同步很重要。1.出现情况的例⼦//定义⼀个全局变量int g_Num = 0;UINT __cdecl ThreadProc(LPVOID pParam){ //线程函数的⽬的是先进⾏累加在进⾏⾃减,程序执⾏完应该是g_Num = 0,对吧 //但是运⾏情况会是什么情况呢? for (int idx = 0;idx<100;++idx) { g_Num = g_Num + 1; CString strNum; Sleep(5); (_T("%d"), g_Num); g_Num = g_Num - 1; } return 0;}void CThreadSynDlg::OnBnClickedThreadsynBut(){ //连续开辟了50个线程,每个线程的线程函数都是ThreadProc for (int idx = 1; idx<=50;++idx) { AfxBeginThread(ThreadProc, NULL); }
}void CThreadSynDlg::OnBnClickedResultBut(){ int realNum = g_Num; //经过调试会发现,得到的值都不是0,其原因就是线程在执⾏过程中可能就会被暂停 //导致结果会不⼀样} 2.解决同步问题的⽅法2.1原⼦互锁家族函数简单来说,这种保护⽅法是基于,⼀旦⼀个线程去修改这个全局变量就不 允许其他线程再去修改这个全局变量。主要有⼀下函数: ① InterlockedIncrement 加1操作 ② InterlockedDecrement 减1操作 ③ InterlockedExchangeAdd加上“指定”的值,可以加上⼀个负数; ④ InterlockedExchange、InterlockedExchangePointer能够以原⼦操作的⽅式⽤第⼆个参数的值来取代第⼀个参数的值;还有很多可以参数微软的⽂档及MSDN。⼀般情况下,在多线程中如果对于某⼀个变量的值进⾏改变的话使⽤以上的互锁函数⽐较⽅便,但是很多时候应⽤场合很复杂,⽐如对⼀个结构体进⾏操作,对类进⾏操作,对链表进⾏插⼊等等,上⾯的⽅法就⽆法满⾜了,需要引⼊更⾼级的⽅法及:Critical Sections(关键代码段、关键区域、临界区域)
//定义⼀个全局变量int g_Num = 0;UINT __cdecl ThreadProc(LPVOID pParam){ //线程函数的⽬的是先进⾏累加在进⾏⾃减,程序执⾏完应该是g_Num = 0,对吧 //但是运⾏情况会是什么情况呢? for (int idx = 0;idx<100;++idx) { //g_Num = g_Num + 1; InterlockedIncrement((LONG*)&g_Num); CString strNum; Sleep(5); (_T("%d"), g_Num); //g_Num = g_Num - 1; InterlockedDecrement((LONG*)&g_Num); } return 0;}void CThreadSynDlg::OnBnClickedThreadsynBut(){ //连续开辟了50个线程,每个线程的线程函数都是ThreadProc for (int idx = 1; idx<=50;++idx) { AfxBeginThread(ThreadProc, NULL); }
}void CThreadSynDlg::OnBnClickedResultBut(){ int realNum = g_Num; //经过调试会发现,得到的值都是0,其原因就是线程在执⾏过程中使⽤了原⼦锁进⾏操作,保证了安全性}2.2Critical Sections(关键代码段、关键区域、临界区域)//定义⼀个全局对象CStringArray g_ArrString;UINT __cdecl ThreadProc(LPVOID pParam){ //每个线程函数的⽬的是在⾃⾝的idx的基础上进⾏100次循环累加,每次 //循环都会把数字转换成字符串存储在动态数组对象中g_ArrString, //因为每个线程都会向数组中添加100个字符串,总共50个线程,如果程序正常执⾏那么字符串数组的元素个数应该为5000 //但是运⾏结果会是什么情况呢? int startIdx = (int)pParam; for (int idx = startIdx;idx< startIdx+100;++idx) { CString str; Sleep(1); (_T("%d"), idx); g_(str); } return 0;}void CThreadSynDlg::OnBnClickedThreadsynBut(){ //连续开辟了50个线程,每个线程的线程函数都是ThreadProc //传⼊的参数为每个线程idx乘上10 for (int idx = 1; idx<=50;++idx) { AfxBeginThread(ThreadProc, (LPVOID)(idx*10)); } //结果是虽然编译通过了,但是在执⾏过程中出错了,出现了异常 //主要原因是多个线程同时操作动态数组,导致数组操作异常,因此需要 //线程同步才能解决,同时使⽤原⼦⽅法是⽆法解决了,因此需要引⼊新的 //解决⽅法即Critical Sections(关键代码段、关键区域、临界区域)
}Critical Sections(关键代码段、关键区域、临界区域)使⽤⽅法建⽴⼀个Critical Sections对象1.初始化: InitializeCriticalSection()2.删除:DeleteCriticalSection()3.进⼊:EnterCriticalSection() (可能造成阻塞,原因是⼀旦有⼀个线程使⽤了这个,其他线程在使⽤,只能等待之前使⽤ 这个的离开 也就是接到5的信号,那么才能开始执⾏,中间会出现卡顿)4.尝试进⼊:TryEnterCriticalSection() (不会造成阻塞)5.离开:LeaveCriticalSection();//详细的请参考微软MSDN的⽂档,搜索同步数据结构//定义⼀个全局对象CStringArray g_ArrString;//定义⼀个Critical Sections的对象CRITICAL_SECTION g_CS;UINT __cdecl ThreadProc(LPVOID pParam){ //每个线程函数的⽬的是在⾃⾝的idx的基础上进⾏100次循环累加,每次 //循环都会把数字转换成字符串存储在动态数组对象中g_ArrString, //因为每个线程都会向数组中添加100个字符串,总共50个线程,如果程序正常执⾏那么字符串数组的元素个数应该为5000 //但是运⾏结果会是什么情况呢? int startIdx = (int)pParam; int startIdx = (int)pParam; for (int idx = startIdx;idx< startIdx+100;++idx) { CString str; //Sleep(1); (_T("%d"), idx); //因为修改数组的是g_(str);,所以在他前⾯加⼊即可,后⾯离开 EnterCriticalSection(&g_CS); g_(str); LeaveCriticalSection(&g_CS); } return 0;}void CThreadSynDlg::OnBnClickedThreadsynBut(){ //连续开辟了50个线程,每个线程的线程函数都是ThreadProc //传⼊的参数为每个线程idx乘上10 //初始化关键代码段,同时记得删除 InitializeCriticalSection(&g_CS); for (int idx = 1; idx<=50;++idx) {
AfxBeginThread(ThreadProc, (LPVOID)(idx*10)); } //结果是虽然编译通过了,但是在执⾏过程中出错了,出现了异常 //主要原因是多个线程同时操作动态数组,导致数组操作异常,因此需要 //线程同步才能解决,同时使⽤原⼦⽅法是⽆法解决了,因此需要引⼊新的 //解决⽅法即Critical Sections(关键代码段、关键区域、临界区域)
}//InitializeCriticalSection()//DeleteCriticalSection()//EnterCriticalSection()//TryEnterCriticalSection()//LeaveCriticalSection();void CThreadSynDlg::OnBnClickedResultBut(){ //这⾥的主要⽬的是为了显⽰计数的个数 CString strCount; INT_PTR nCount = g_nt(); (_T("%d"), nCount); MessageBox(strCount); for (INT_PTR idx=0;idx 固有特点:1.是⼀个⽤户模式的对象,不是系统的核⼼对象;2.因为不是核⼼对象,所以执⾏速度快、有效率3.因为不是核⼼对象,所以不能跨进程使⽤4.可以多次“进⼊”,但必须多次“退出”5.最好不要同时进⼊或等到多个Critical Sections,容易造成死锁; 什么是死锁呢?简单来说加⼊开启两个Critical Sections,第⼀线程进⼊第⼀个Critical Sections1,第⼆个线程进⼊ Critical Sections2,但是同时呢,进⼊的Critical Sections1的线程⼜要打算进⼊Critical Sections2,⽽进⼊Critical Sections2的线程需要 进⼊ Critical Sections1,结果这两个线程都在等,这样进⼊死循环了,各⾃出不来,导致死锁,只有线程死了才能解脱所以叫死锁。 所以避免死锁的最好办法就是进来少建⽴Critical Sections对象,这样就避免死锁了。6.⽆法检测到进⼊到Critical Sections⾥⾯的线程当前是否已经退出,所以在Critical Sections尽量不要执⾏耗时的操作,,,那么还有没有更好的⽅法进⾏线程间同步呢?当然有下⾯就介绍互斥器Mutex 2.3 互斥器Mutex互斥器和2.2的关键代码段⽅法不同,他是系统的核⼼对象,所以速度上要⽐关键代码段⽅法慢点,当然他也有它的优点,使⽤⽅法⽅⾯和关键代码段⽅法差不多,我们来看看: 使⽤⽅法: 1.创建⼀个互斥器:CreateMutex; 2.打开⼀个已经存在的互斥器:OpenMutex; 3.获得互斥器的拥有权:WaitForSingleObject(),WaitForMultipleObjects(),,,等⼀类等待的函数,但是可能造成阻塞; 4.释放互斥器拥有权:ReleaseMutex; 5.关闭互斥器:CloseHandle; 具体例⼦如下: 创建是在 void CThreadSynDlg::OnBnClickedThreadsynBut()函数中销毁是在 void CThreadSynDlg::OnBnClickedResultBut()函数中 处理是在线程函数中。//定义⼀个全局对象CStringArray g_ArrString;//定义⼀个互斥器Mutex的句柄HANDLE ghMutex = NULL;UINT __cdecl ThreadProc(LPVOID pParam){ //每个线程函数的⽬的是在⾃⾝的idx的基础上进⾏100次循环累加,每次 //循环都会把数字转换成字符串存储在动态数组对象中g_ArrString, //因为每个线程都会向数组中添加100个字符串,总共50个线程,如果程序正常执⾏那么字符串数组的元素个数应该为5000 //但是运⾏结果会是什么情况呢? int startIdx = (int)pParam; for (int idx = startIdx;idx< startIdx+100;++idx) { CString str; //Sleep(1); (_T("%d"), idx); //定义⼀个单⼀的等待函数,第⼀次参数是互斥器的对象,第⼆个是等待时间,这⾥是⼀直等待 //因为现在的互斥器是激活态,这线程⼀旦调⽤⽴刻返回,此时互斥器进⼊⾮激活态,如果这时候 //有其他线程也是⽤这个WaitForSingleObject函数不会⽴刻返回,只能等待互斥器的在此激活 DWORD dwWaitResult = WaitForSingleObject(ghMutex, INFINITE); switch (dwWaitResult) { case WAIT_ABANDONED://这个其实就是没⽤获得互斥器的激活态,或者其他的线程正在使⽤ case WAIT_OBJECT_0://如果返回的是该值,则说明当前的互斥器已经被当前线程拥有了,可以进场操作了 g_(str); ReleaseMutex(ghMutex);//如果处理完,就释放⼀下,使互斥器激活态,以便其他线程可以使⽤ break; } } return 0;}void CThreadSynDlg::OnBnClickedThreadsynBut(){ //连续开辟了50个线程,每个线程的线程函数都是ThreadProc //传⼊的参数为每个线程idx乘上10 //1.创建⼀个互斥器:CreateMutex;同时记得释放句柄 //⼀旦创建成功,则处于激活态,也就是现在没有任何⼀个线程在调⽤它,可以谁是等待线程的调⽤ //⼀旦线程线程调⽤则该互斥器⽴刻处于⾮激活态,其他线程在调⽤只有等待该互斥器的在此激活 ghMutex = CreateMutex(NULL, FALSE, NULL); for (int idx = 1; idx<=50;++idx) { { AfxBeginThread(ThreadProc, (LPVOID)(idx*10)); } }//InitializeCriticalSection()//DeleteCriticalSection()//EnterCriticalSection()//TryEnterCriticalSection()//LeaveCriticalSection();void CThreadSynDlg::OnBnClickedResultBut(){ //这⾥的主要⽬的是为了显⽰计数的个数 CString strCount; INT_PTR nCount = g_nt(); (_T("%d"), nCount); MessageBox(strCount); for (INT_PTR idx=0;idx 互斥器的特点:1.是⼀个系统核⼼对象,所以有安全描述指针,⽤完了要CloseHandle关闭句柄,这些是内核对象的共同特征;2.因为是核⼼对象,所以执⾏速度会⽐Critical Sections慢⼏乎100倍的时间3.因为是核⼼对象,⽽且可以命名,所以可以跨进程使⽤;使⽤正确的情况下不会发⽣死锁;5.在“等待”⼀个Mutex的时候,可以指定“结束等待”的时间长度;6.可以检测到当前拥有互斥器所有权的线程是否已经退出!wait。。。。函数会返回:WAIT_ABANDONED 2.4 Semaphores(信号量)和前⼏个的处理⽅法不同,我们从前⾯的⽅法中可以看到,其实本质上都同⼀个时间只有⼀个线程处理⼀个对象,⽽信号量和他们的区别在与信号量的是同⼀个时间多个线程同时处理多个对象,这⾥⼤家需要理解,前⾯的都是⼀对⼀,⽽信号量是多对多。使⽤⽅法: 1.创建⼀个信号量:CreateSemaphore(); 2.打开⼀个已经存在的信号量:OpenSemaphore(); 3.获得信号量的⼀个占有权:WaitForSingleObject()、 WaitForMultipleObjects()等⼀类等待函数。。。。。可能造成阻塞 4.释放信号量的占有权:ReleaseSemaphore() 5.关闭信号量:CloseHandle()HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName );lpSemaphoreAttributes[输⼊]设置为NULL。lInitialCount[in]指定信号量对象的初始计数。此值必须⼤于或等于零且⼩于或等于lMaximumCount。当信号量的计数⼤于零时,将发出信号状态;在信号量为零时,将不发出信号状态。每当等待函数释放等待信号量的线程时,计数就会减少⼀。通过调⽤ReleaseSemaphore函数将计数增加指定的数量。lMaximumCount[in]指定信号量对象的最⼤计数。该值必须⼤于零。lpName[in]指向以空值结尾的字符串的长指针,该字符串指定信号量对象的名称。名称限制为MAX_PATH字符,并且可以包含除反斜杠路径分隔符()之外的任何字符。名称⽐较区分⼤⼩写。如果lpName与现有命名信号对象的名称匹配,则lInitialCount和lMaximumCount参数将被忽略,因为它们已在创建过程中设置。每种对象类型(例如内存映射,信号量,事件,消息队列,互斥体和看门狗计时器)都有其⾃⼰单独的名称空间。空字符串(“”)被视为命名对象。在基于Windows桌⾯的平台上,同步对象都共享相同的名称空间。 //定义⼀个全局对象CStringArray g_ArrString;//定义⼀个信号量的全局句柄HANDLE ghSemaphore = NULL;UINT __cdecl ThreadProc(LPVOID pParam){ //每个线程函数的⽬的是在⾃⾝的idx的基础上进⾏100次循环累加,每次 //每个线程函数的⽬的是在⾃⾝的idx的基础上进⾏100次循环累加,每次 //循环都会把数字转换成字符串存储在动态数组对象中g_ArrString, //因为每个线程都会向数组中添加100个字符串,总共50个线程,如果程序正常执⾏那么字符串数组的元素个数应该为5000 //但是运⾏结果会是什么情况呢? int startIdx = (int)pParam; CString strOut; while (TRUE) { //因为现在的互斥器是激活态,这线程⼀旦调⽤⽴刻返回,此时互斥器进⼊⾮激活态,如果这时候 //有其他线程也是⽤这个WaitForSingleObject函数会⽴刻返回,因为这是多个线程可以同时进⾏操作 //这⾥不同的是不需要等待,因为信号量可以多个线程同时操作他 DWORD dwWaitResult = WaitForSingleObject(ghSemaphore, 0); switch (dwWaitResult) { case WAIT_OBJECT_0://如果返回的是该值,则说明当前信号量已经被当前线程拥有了,可以进场操作了 (_T("Thred %d: wait succeeded!"), GetCurrentThreadId()); OutputDebugString(strOut); /* 可以加⼊其它要⼲的活 */ ReleaseSemaphore(ghSemaphore,1,NULL);//如果处理完,就释放⼀下 break; case WAIT_TIMEOUT: (_T("Thread %d: wait timed out!"), GetCurrentThreadId()); OutputDebugString(strOut); break; } } return 0;}//CreateSemaphore//OpenSemaphore//WaitForSingleObject() WaitForMultipleObjects()//ReleaseSemaphore()void CThreadSynDlg::OnBnClickedThreadsynBut(){ //连续开辟了50个线程,每个线程的线程函数都是ThreadProc //传⼊的参数为每个线程idx乘上10 //1.创建信号量ghSemaphore = CreateSemaphore(NULL, 10, 10, NULL); //其中在创建的过程中我们需要告诉他多少个线程访问多少个对象 //第三个参数是有多少个对象可以执⾏ //第⼆个参数是初始化时有多少个对象可以执⾏,⼀般要⼩于等于第三个参数的值且⼤于等于0 ghSemaphore = CreateSemaphore(NULL, 10, 10, NULL); for (int idx = 1; idx<=20;++idx) { AfxBeginThread(ThreadProc, (LPVOID)(idx*10)); } }//InitializeCriticalSection()//DeleteCriticalSection()//EnterCriticalSection()//TryEnterCriticalSection()//LeaveCriticalSection();void CThreadSynDlg::OnBnClickedResultBut(){ //这⾥的主要⽬的是为了显⽰计数的个数 CString strCount; INT_PTR nCount = g_nt(); INT_PTR nCount = g_nt(); (_T("%d"), nCount); MessageBox(strCount); for (INT_PTR idx=0;idx 互斥器的特点:1.是⼀个系统核⼼对象,所以有安全描述指针,⽤完了要CloseHandle关闭句柄,这些是内核对象的共同特征;2.因为是核⼼对象,所以执⾏速度会⽐Critical Sections慢⼏乎100倍的时间(相对⽽⾔,现在的cpu很快了,⼏乎没差别了)3.因为是核⼼对象,⽽且可以命名,所以可以跨进程使⽤;ore使⽤正确的情况下不会发⽣死锁;5.在“等待”⼀个Semaphore的时候,可以指定“结束等待”的时间长度;6.⾮排他性的占有,跟Critical Sections和Mutex不同,这两种⽽⾔是排他性的占有,即同⼀时间内只能有单⼀的线程获得⽬标并拥有操作的权利,⽽Semaphores则不是这样的,同⼀时间可以有多个线程获得⽬标并进⾏操作 如果上⾯程序使⽤信号量⽅式去做向CStringArray中添加节点的同步可以吗:答案是可以的,此时只需要修改: CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName );的第三个参数为1即可,也就是说每次只允许⼀个线程对象对其操作,这样设置就和前⾯的互斥锁没什么两样了。 2.5 Event Objects(事件)Event ⽅式是最具有弹性的同步机制,因为他的状态完全由你决定,不会像Mutex和Semaphores的状态⼀样会根据waitforsingleObject----等类似的调⽤⽽改变,所以你需要精确的告诉Event对象该做什么事,以及什么时候去做. 使⽤⽅法: 1.创建⼀个事件对象:CreateEvent; 2.打开⼀个已经存在的事件对象:OpenEvent 3.获得事件的占有权:waitforsingleObject等函数,可能会阻塞 4.释放事件的占有权(设置为激发状态,以让其他等待的线程苏醒:setEvent 5.⼿动设置为⾮激发态,ResetEvent 6.关闭事件对象的句柄:closehandle 特点:1.是⼀个系统核⼼对象,所以有安全描述指针,⽤完了要CloseHandle关闭句柄,这些是内核对象的共同特征;2.因为是核⼼对象,所以执⾏速度会⽐Critical Sections慢⼏乎100倍的时间(相对⽽⾔,现在的cpu很快了,⼏乎没差别了)3.因为是核⼼对象,⽽且可以命名,所以可以跨进程使⽤;4.通常被⽤于overlapped I/O或者被⽤来设计某些⾃定义的同步对象。 #include HANDLE ghWriteEvent; HANDLE ghWriteEvent; HANDLE ghThreads[THREADCOUNT];DWORD WINAPI ThreadProc(LPVOID);void CreateEventsAndThreads(void) { int i; DWORD dwThreadID; // Create a manual-reset event object. The write thread sets this // object to the signaled state when it finishes writing to a // shared buffer. ghWriteEvent = CreateEvent( NULL, // default security attributes TRUE, // manual-reset event FALSE, // initial state is nonsignaled TEXT("WriteEvent") // object name ); if (ghWriteEvent == NULL) { printf("CreateEvent failed (%d)n", GetLastError()); return; } // Create multiple threads to read from the buffer. for(i = 0; i < THREADCOUNT; i++) { // TODO: More complex scenarios may require use of a parameter // to the thread procedure, such as an event per thread to // be used for synchronization. ghThreads[i] = CreateThread( NULL, // default security 0, // default stack size ThreadProc, // name of the thread function NULL, // no thread parameters 0, // default startup flags &dwThreadID); if (ghThreads[i] == NULL) { printf("CreateThread failed (%d)n", GetLastError()); return; } }}void WriteToBuffer(VOID) { // TODO: Write to the shared buffer. printf("Main thread writing to the "); // Set ghWriteEvent to signaled if (! SetEvent(ghWriteEvent) ) { printf("SetEvent failed (%d)n", GetLastError()); return; }}void CloseEvents()void CloseEvents(){ // Close all event handles (currently, only one global handle). CloseHandle(ghWriteEvent);}int main( void ){ DWORD dwWaitResult; // TODO: Create the shared buffer // Create events and THREADCOUNT threads to read from the buffer CreateEventsAndThreads(); // At this point, the reader threads have started and are most // likely waiting for the global event to be signaled. However, // it is safe to write to the buffer because the event is a // manual-reset event. WriteToBuffer(); printf("Main thread waiting for threads "); // The handle for each thread is signaled when the thread is // terminated. dwWaitResult = WaitForMultipleObjects( THREADCOUNT, // number of handles in array ghThreads, // array of thread handles TRUE, // wait until all are signaled INFINITE); switch (dwWaitResult) { // All thread objects were signaled case WAIT_OBJECT_0: printf("All threads ended, cleaning up for "); break; // An error occurred default: printf("WaitForMultipleObjects failed (%d)n", GetLastError()); return 1; } // Close the events to clean up CloseEvents(); return 0;}DWORD WINAPI ThreadProc(LPVOID lpParam) { // lpParam not used in this example. UNREFERENCED_PARAMETER(lpParam); DWORD dwWaitResult; printf("Thread %d waiting for ", GetCurrentThreadId()); dwWaitResult = WaitForSingleObject( ghWriteEvent, // event handle INFINITE); // indefinite wait INFINITE); // indefinite wait switch (dwWaitResult) { // Event object was signaled case WAIT_OBJECT_0: // // TODO: Read from the shared buffer // printf("Thread %d reading from buffern", GetCurrentThreadId()); break; // An error occurred default: printf("Wait error (%d)n", GetLastError()); return 0; } // Now that we are done reading the buffer, we could use another // event to signal that this thread is no longer reading. This // example simply uses the thread handle for synchronization (the // handle is signaled when the thread terminates.) printf("Thread %d exitingn", GetCurrentThreadId()); return 1;}
发布者:admin,转转请注明出处:http://www.yc00.com/news/1687426649a9206.html
评论列表(0条)