关于mfc线程 的退出问题、同步问题

本文详细介绍了MFC中的线程创建与管理方法,并深入探讨了多种线程同步技术,包括临界区、互斥区、事件和信号量等。通过具体的代码示例讲解了这些同步机制的使用方法及注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

AfxBeginThread()函数的返回值是CWinThread* 指针,但是这个指针不能直接使用,因为这个指针会自动销毁。如果道友直接使用了这个指针,那么当在操作这个指针时,若已被mfc销毁,那么访问违规将会到来。
至于返回值的使用请看,我写的一个mfc程序片段。
[html]  view plain  copy
  1. CString strName = _T("");  
  2. CWinThread* pThread = NULL;  
  3. UINT CBcgTestDlg::ThreadWorkFunc(LPVOID lPvoid)  
  4. {  
  5.       
  6.     for (int n = 0; n < 10000; n++)  
  7.     {  
  8.         strName = _T("http://blog.csdn.net/windows_nt");  
  9.         strName = strName + _T("\n");  
  10.         TRACE(strName);  
  11.     }  
  12.       
  13.     return 0;  
  14. }  
  15.   
  16.   
  17. void CBcgTestDlg::OnOK()   
  18. {  
  19.     for (int n = 0; n < 10000; ++n)  
  20.     {  
  21.         if (pThread)  
  22.         {  
  23.             WaitForSingleObject(pThread->m_hThread, INFINITE);  
  24.             delete pThread;  
  25.         }  
  26.           
  27.         pThread = AfxBeginThread(ThreadWorkFunc, NULL, 0, CREATE_SUSPENDED, NULL);  
  28.         if (pThread)  
  29.         {  
  30.             pThread->m_bAutoDelete = FALSE;  
  31.             pThread->ResumeThread();  
  32.         }  
  33.     }  
  34. }  
  35.   

  1. //现在可以放心的使用返回值pThread了,但是要记得在使用结束后记得调用delete pThread,释放资源(CWinThread类中的线程句柄会在析构函数中自动释放)。 

线程同步的方式主要有:临界区、互斥区、事件、信号量四种方式。
一、

接下来我主要讲一下自己在学习windows核心编程中对于临界区线程同步方式的使用。

临界区线程同步在windows核心编程中被称为关键段线程同步,以下统称关键段
关键段是一小段代码,它在执行之前需要独占对一些资源的访问权。
缺点:能且只能用在一个进程中的多线程同步。可能陷入死锁,因为我们无法为进入关键段的线程设置最大等待时间。

接下来我介绍一些关键段线程同步的使用
先看一个事例代码

[html]  view plain  copy
  1. int g_nSum = 0;  
  2. CRITICAL_SECTION g_cs;  
  3.   
  4. DWORD WINAPI FirstThread(PVOID pvParam)  
  5. {  
  6.   EnterCriticalSection(&g_cs);  
  7.   g_nSum = 0;  
  8.   for (int n = 0; n < 10000; ++n)  
  9.   {  
  10.     g_nSum += n;  
  11.   }  
  12.   LeaveCriticalSection(&g_cs);  
  13.   return g_nSum;  
  14. }  
  15.   
  16. DWORD WINAPI SecondThread(PVOID pvParam)  
  17. {  
  18.   EnterCriticalSection(&g_cs);  
  19.   g_nSum = 0;  
  20.   for (int n = 0; n < 10000; ++n)  
  21.   {  
  22.     g_nSum += n;  
  23.   }  
  24.   LeaveCriticalSection(&g_cs);  
  25.   return g_nSum;  
  26. }  
在使用关键段(CRITICAL_SECTION)时,只有两个必要条件:
1、想要访问资源的线程必须知道用来保护资源的CRITICAL_SECTION对象地址。
CRITICAL_SECTION对象可以作为全局对象来分配,也可以作为局部对象来分配,
或者从堆中动态地分配。
2、如何线程在试图访问被保护的资源之前,必须对CRITICAL_SECTION结构的内部
成员进行初始化。


关键段线程同步常用函数介绍
[html]  view plain  copy
  1. //1、首先我们要分配一个CRITICAL_SECTION对象,并进行初始化(使用关键段同步的线程必须调用此函数)  
  2. void InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection )  
  3.   
  4. //2、当知道线程将不再需要访问共享资源时,我们应该调用下边的函数来清理CRITICAL_SECTION结构  
  5. void DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection )  
  6.   
  7. //3、在对保护的资源进行访问之前,必须调用下面的函数  
  8. void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection )  
  9. //可以对上边的函数多次调用,表示调用线程被获准访问的次数  
  10.   
  11. //4、也可以用下边的函数代替EnterCriticalSection  
  12. BOOL TryEnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection )  
  13. //通过返回值判断当前线程是否获准访问资源,线程永远不会进入等待状态,如果  
  14. //返回TRUE表示该线程获准并正在访问资源,离开时必须调用LeaveCriticalSection()  
  15.   
  16. //5、在代码完成对资源的访问后,必须调用以下函数,释放访问权限  
  17. void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection )  
  18. //转载请注明文章来自:http://blog.csdn.net/windows_nt  
以上访问方式(EnterCriticalSection方式)可能会使调用线程切换到等待状态,这意味着线程必须从用户模式切换到内核模式,这个切换开销非常大。为了提高关键段的性能,Microsoft把旋转锁合并到了关键段中。因此,当调用EnterCriticalSection的时候,它会用一个旋转锁不断地循环,尝试在一段时间内获得对资源的访问权,只有当尝试失败时,线程才会切换到内核模式并进入等待状态。

[html]  view plain  copy
  1. //1、为了在使用关键段的时候同时使用旋转锁,我们必须调用下面的函数来初始化关键段  
  2. BOOL InitializeCriticalSectionAndSpinCount( LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount )  
  3. //第二个参数dwSpinCount表示我们希望旋转锁循环的次数。  
  4.   
  5. //2、我们也可以调用下面的函数来改变关键段的旋转次数  
  6. DWORD SetCriticalSectionSpinCount( LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount )  
  7. //如果主机只有一个处理器,函数会忽略dwSpinCount参数  

Slim读/写锁

SRWLock允许我们区分那些想要读取资源的值的线程(读取者线程)和想要更新资源的值的线程(写入者线程)。
[html]  view plain  copy
  1. //1、首先我们要分配一个SRWLOCK对象并用下边函数初始化它。  
  2. void InitializeSRWLock( __out PSRWLOCK SRWLock )  
  3.   
  4. //2、请求对保护资源的独占访问权(写权限)  
  5. void AcquireSRWLockExclusive( __inout PSRWLOCK SRWLock )  
  6.   
  7. //3、完成对资源的更新后,应该解除对资源的锁定  
  8. void ReleaseSRWLockExclusive( __inout PSRWLOCK SRWLock )  
  9.   
  10. //4、对应的读者线程函数如下  
  11. void AcquireSRWLockShared( __inout PSRWLOCK SRWLock )  
  12. void ReleaseSRWLockShared( __inout PSRWLOCK SRWLock )  
与关键段相比,PSRWLOCK缺乏下面两个特性
1、不存在TryEnter(Share/Exclusive)SRWLock之类的函数,如果锁已经被占用,那么调用会阻塞调用线程
2、不能递归的获得PSRWLOCK。也就是说,一个线程不能为了多次写入资源而多次锁定 资源,如后再多次释放对资源的锁定。

线程同步性能排序(由高到低)
volatile读取 -->volatile写入-->Interlocked API(原子方式)-->SRWLock-->关键段-->内核对象

二、互斥器(Mutexes)的用途和临界区(critical section)的用途非常相似,如:一个时间内只能够有一个线程拥有mutex,就好像同一时间内只能够有一个线程进入同一个critical section一样。但是mutex通过牺牲速度,提高了灵活性,功能变得更加强大了。

虽然mutex和critical section做相同的事情,但它们的运作还是有差别的:
1、锁住一个未被拥有的mutex,比锁住一个未被拥有的critical section需要花费几乎100倍的时间。
2、mutex可以跨进程使用。critical section则只能在同一个进程中使用。

3、等待一个mutex时,你可以指定“结束等待”的世间长度,但对于critical section则不行。


造成以上差别的主要原因是:mutex是内核对象,critical section非内核对象。

以下是mutex和critical section的相关函数比较:

临界区互斥器
CRITICAL_SECTION
InitializeCriticalSection()
CreateMutex()
OpenMutex()
EnterCriticalSection()WaitForSingleObject()
WaitForMultipleObjects()
MsgWaitForMultipleObjects()
LeaveCriticalSection()ReleaseMutex()
DeleteCriticalSection()CloseHandle()

使用mutex时注意:

在一个适当的程序中,线程绝对不应该在它即将结束前还拥有一个mutex,因为这意味着线程没有能够适当地清除其资源。不幸的是,我们并不身处一个完美的世界,有时候,因为某种理由,线程可能没有在结束前调用ReleaseMutex()。为了解决这个问题,mutex有一个非常重要的特性。这性质在各种同步机制中是独一无二的,如果线程拥有一个mutex而在结束前没有调用ReleaseMutex(),mutex不会被摧毁,取而代之的是,该mutex会被视为“未被拥有”以及“未被激发”,而下一个等待中的线程会被以WAIT_ABANDONED_0通知。无论线程是因为ExitThread()而结束,或是因当掉而结束,这种情况都存在。


任何时候只要你想锁住超过一个以上的同步对象,你就有死锁的潜在病因。
如果总是在相同时间把所有对象都锁住,问题可去矣。
事例如下,存在潜在死锁的可能:

[html]  view plain  copy
  1. void SwapLists(List* list1, List* list2)  
  2. {  
  3.     EnterCriticalSection(list1->critical_sec);  
  4.     EnterCriticalSection(list2->critical_sec);  
  5.     //list1,list2数据交换  
  6.     LeaveCriticalSection(list1->critical_sec);  
  7.     LeaveCriticalSection(list2->critical_sec);  
  8. }  
正确的做法:
[html]  view plain  copy
  1. void SwapLists(List* list1, List* list2)  
  2. {  
  3.     HANDLE arrHandles[2];  
  4.     arrHandles[0] = list1->hMutex;  
  5.     arrHandles[1] = list2->hMutex;  
  6.     WaitForMultipleObjects(2, arrHandles, TRUE, INFINITE);  
  7.     //list1,list2数据交换  
  8.     ReleaseMutex(arrHandles[0]);  
  9.     ReleaseMutex(arrHandles[1]);  
  10. }  

三、

前边讲过了互斥器线程同步-----windows核心编程-互斥器(Mutexes),这章我来介绍一下信号量(semaphore)线程同步。

理论上说,mutex是semaphore的一种退化。如果你产生一个semaphore并令最大值为1,那就是一个mutex。也因此,mutex又常被称为binary semaphore。如果某个线程拥有一个binary semaphore,那么就没有其他线程能够获得其拥有权。但是在win32中,这两种东西的拥有权的意义完全不同,所以它们不能够交换使用,semaphore不像mutex,它并没有所谓的“wait abandoned”状态可以被其他线程侦测到。

每当一个锁定动作成功,semaphore的现值就会减1,你可以使用任何一种wait...()函数来要求锁定一个semaphore。如果semaphore的现值不为0,wait...()函数会立刻返回,这和mutex很像,当没有任何线程拥有mutex,wait...()函数会立刻返回。

注意,如果锁定成功,你也不会收到semaphore的拥有权。因为可以有一个以上的线程同时锁定一个semaphore。所以谈semaphore的拥有权并没有太多实际意义。在semaphore身上并没有所谓“独占锁定”这种事情。也因为没有所有权的观念,一个线程可以反复调用wait...()函数以产生新的锁定。这和mutex绝不相同:拥有mutex的线程不论再调用多少次wait...()函数,也不会被阻塞住。

与mutex不同的是,调用ReleaseSemaphore()的那个线程,并不一定就得是调用wait...()的那个线程。
任何线程都可以在任何时间调用ReleaseSemaphore(),解除被任何线程锁定的semaphore。

以下是我对三种同步方式中,常用到的函数的总结。

临界区互斥器信号量
CRITICAL_SECTION
InitializeCriticalSection()
CreateMutex()
OpenMutex()
CreateSemaphore
EnterCriticalSection()WaitForSingleObject()
WaitForMultipleObjects()
MsgWaitForMultipleObjects()
WaitForSingleObject()
WaitForMultipleObjects()
MsgWaitForMultipleObjects()
...
LeaveCriticalSection()ReleaseMutex()ReleaseSemaphore()
DeleteCriticalSection()CloseHandle()CloseHandle()

事件线程同步----- window核心编程-内核对象线程同步


四、

上一章讲了关键字(临界区)线程同步,使用关键字线程同步,我们很容易陷入死锁的情形,这是因为我们无法为进入关键段指定一个最长等待时间。

本章将讨论如何使用内核对象来对线程同步。我们也将看到,与用户模式下的同步机制(关键段同步)相比,内核对象的用途要广泛的多。实际上,内核对象唯一的缺点就是它们的性能。内核对象包括进程、线程以及作业,几乎所有这些内核对象都可以用来进行同步。对线程同步来书,这些内核对象中的每一种要么处于触发状态,要么处于未触发状态。Microsoft为每种对象创建了一些规则,规定如何在这两种状态之间进行转换。例如,进程内核对象在创建的时候总是处于未触发状态。当进程终止的时候,操作系统会自动使进程内核对象变成触发状态。当进程内核对象被触发后,它将永远保持这种状态,再也不会变回到未触发状态。在进程内核对象的内部有一个布尔变量,当系统创建内核对象的时候会把这个变量的值初始化为false(未触发)。当进程终止的时候,操作系统会自动把相应的内核对象中的这个布尔值设为true,表示该对象已经被触发。


下边讲一些内核同步中用到的函数。
等该函数使一个线程自愿进入等待状态,直到指定的内核对象被触发为止。

DWORD waitForSingleObject(HANDLE hObject, DWORD dwMilliseconds);
//hObject:内核对象句柄
//dwMilliseconds等待时间ms为单位,INFINITE为一直等待,只到内核对象被触发为止。

DWORD WaitForMultipleObjects( DWORD nCount, CONST HANDLE *lpHandles, BOOL bWaitAll, DWORD dwMilliseconds );
//waitForSingleObject和WaitForMultipleObjects相似,唯一的不同之处在于它允许调用线程同时检查多个内核对象的触发状态

/*函数的返回值告诉调用方函数为什么它得以继续运行。如果给bWaitAll传的是FALSE,那么只要任何一个对象被触发,函数就会立即返回。这时的返回值是WAIT_OBJECT_0和(WAIT_OBJECT_0+dwCount-1)之间的任何一个值。换句话说,如果返回值既不是WAIT_TIMEOUT,也不是WAIT_FAILED,那么我们应该把返回值减去WAIT_OBJECT_0。得到的数值是我们在第二个参数中传的句柄数组的一个索引,用来告诉我们被触发的是那个对象。*/


//下面的事例代码可以更清晰的对此进行解释
[html]  view plain  copy
  1. HANDLE h[3];//我的博客:<a href="http://blog.csdn.net/windows_nt">http://blog.csdn.net/windows_nt</a>  
  2. h[0] = hProcess1;  
  3. h[1] = hProcess2;  
  4. h[2] = hProcess3;  
  5. DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);  
  6. switch(dw)  
  7. {  
  8. case WAIT_FAILED:  
  9.     //Bad call to function (invalid handle)  
  10.     break;  
  11. case WAIT_TIMEOUT:  
  12.     //None of the objects became signaled within 5000 milliseconds  
  13.     break;  
  14. case WAIT_OBJECT_0 + 0:  
  15.     //h[0] signaled, hProcess1 terminated  
  16.     break;  
  17. case WAIT_OBJECT_0 + 1:  
  18.     //h[1] signaled, hProcess2 terminated  
  19.     break;  
  20. case WAIT_OBJECT_0 + 2:  
  21.     //h[2] signaled, hProcess3 terminated  
  22.     break;  
  23. }  

2、事件内核对象
//事件包含一个使用计数(这一点和所有其他内核对象一样),一个用来表示事件是自动重置事件还是手动重置事件的布尔值,以及另一个用来表示事件有没有被触发的布尔值。
//有两种不同类型的事件对象:手动重置事件和自动重置事件。当一个手动重置事件被触发的时候,正在等待该事件的所有线程都将变成可调度状态,而当一个自动重置事件被触发的时候,只有一个正在等待该事件的线程会变成可调用状态。


//创建一个事件内核对象
HANDLE CreateEvent( 
LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性
BOOL bManualReset, //自动重置/手动重置
BOOL bInitialState, //是否触发

LPCSTR lpName );//事件内核对象的名字,可以为空

//新版本

HANDLE CreateEventEx(
LPSECURITY_ATTRIBUTES psa, //安全属性
PCTSTR pszName, //名字
DWORD dwFlags, //是否触发

DWORD dwDesiredAccess)

//dwDesiredAccess:允许我们指定在创建事件时返回的句柄对事件有何种访问权限。这是一种创建事件句柄的新方法,它可以减少权限,相比较而言,CreateEvent()总是被授予全部权限。但CreateEventEx()更有用的地方在于它允许我们以减少权限的方式来打开一个已经存在的事件,而CreateEvent()总是要求全部权限。


//下边这个例子展示了如何使用事件内核对象来对线程进行同步。
[html]  view plain  copy
  1. //Create a global handle to a manual-reset, nonsignaled event.  
  2. HANDLE g_hEvent;  
  3. int WINAPI _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,   
  4.                      LPSTR lpCmdLine, int nShowCmd )  
  5. {  
  6.     //Create the manual-reset, nonsignaled event  
  7.     g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);  
  8.     //Spawn 3 new threads  
  9.     HANDLE hThread[3];  
  10.     DWORD dwThread;  
  11.     hThread[0] = _beginthread(NULL, 0, wordCount, NULL, 0, &dwThread);  
  12.     hThread[1] = _beginthread(NULL, 0, SpellCheck, NULL, 0, &dwThread);  
  13.     hThread[2] = _beginthread(NULL, 0, GrammarCheck, NULL, 0, &dwThread);  
  14.   
  15.   
  16.     OpenFileAndReadContentsIntoMemory(...);  
  17.     //allow all 3 threads to access the memory  
  18.     SetEvent(g_hEvent);  
  19. }  
  20.   
  21.   
  22. DWORD WINAPI wordCount(PVOID pvParam)  
  23. {  
  24.     //Wait until the file's data is in memory  
  25.     waitForSingleObject(g_hEvent, INFINITE);  
  26.     //access the memory block.  
  27.     return 0;  
  28. }  
  29.   
  30.   
  31. DWORD WINAPI SpellCheck(PVOID pvParam)  
  32. {  
  33.     //Wait until the file's data is in memory  
  34.     waitForSingleObject(g_hEvent, INFINITE);  
  35.     //access the memory block.  
  36.     return 0;  
  37. }  
  38.   
  39.   
  40. DWORD WINAPI GrammarCheck(PVOID pvParam)  
  41. {  
  42.     //Wait until the file's data is in memory  
  43.     waitForSingleObject(g_hEvent, INFINITE);  
  44.     //access the memory block.  
  45.     return 0;  
  46. }  

下边是我自己写的一个事例片段,很简单

[html]  view plain  copy
  1. CString strName = _T("");  
  2.   
  3. UINT CBcgTestDlg::ThreadWorkFunc(LPVOID lPvoid)  
  4. {  
  5.       
  6.     for (int n = 0; n < 10000; n++)  
  7.     {  
  8.         strName = _T("http://blog.csdn.net/windows_nt");  
  9.         strName = strName + _T("\n");  
  10.         TRACE(strName);  
  11.     }  
  12.       
  13.     return 0;  
  14. }  
  15.   
  16. void CBcgTestDlg::OnOK()   
  17. {  
  18.     CWinThread* pThread = NULL;  
  19.   
  20.     for (int n = 0; n < 10000; ++n)  
  21.     {  
  22.         if (pThread)  
  23.         {  
  24.             WaitForSingleObject(pThread->m_hThread, INFINITE);  
  25.             delete pThread;  
  26.         }  
  27.           
  28.         pThread = AfxBeginThread(ThreadWorkFunc, NULL, 0, CREATE_SUSPENDED, NULL);  
  29.         if (pThread)  
  30.         {  
  31.             pThread->m_bAutoDelete = FALSE;  
  32.             pThread->ResumeThread();  
  33.         }  
  34.     }  
  35. }  
注意线程函数可以为类函数,但必须是静态函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值