多线程编程的一点小心得(2)

时间:2020-12-05 16:33:18

好咧,今天进入多线程学习的第二天,貌似需要来一点有技术含量的东西了。

多线程在平时使用的过程中,用得比较多的应该就是线程同步了。有一个变量,好几个线程都要用,如果只是读取的话没关系,假如需要对此变量进行修改的话,那就难保线程A尚未处理完该变量,线程B就拿到手了。如果不对这个变量进行一些特殊处理,则有可能出现某两个线程在不同时间拿到的变量值是相同的,如果这是在卖火车票的话,那么08车厢的12D座可能会有5个人在吵架。

这次的演示使用控制台模式会方便一些。如果我们对变量不做任何处理,就建立两个线程来访问、修改它,会有什么结果呢?代码如下:(因为是控制台下的编程,所以写起来要有点小变化)

  
 
 
  1. #include "stdafx.h" 
  2. #include <iostream> 
  3. #include <Windows.h> 
  4.  
  5. int g_nCounter = 0; 
  6.  
  7. UINT WINAPI ThreadProc1(LPVOID lpParameter) 
  8.     while(g_nCounter < 100) 
  9.     { 
  10.         std::cout << "gCounter in thread 1: " << g_nCounter++ << std::endl; 
  11. Sleep(10);
  12.     } 
  13.     return 0; 
  14.  
  15. UINT WINAPI ThreadProc2(LPVOID lpParameter) 
  16.     while(g_nCounter < 100) 
  17.     { 
  18.         std::cout << "gCounter in thread 2: " << g_nCounter++ << std::endl;
  19. Sleep(10);  
  20.     } 
  21.     return 0; 
  22.  
  23. int main(int argc, char* argv[]) 
  24.     HANDLE hThread1; 
  25.     HANDLE hThread2; 
  26.  
  27.     hThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc1, NULL, 0, NULL); 
  28.     hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc2, NULL, 0, NULL); 
  29.  
  30.     Sleep(2000); 
  31.  
  32.   CloseHandle(hThread1);
  33. CloseHandle(hThread2);
  34.     return 0; 

gCounter是全局变量,两个线程都可以访问。那么结果如下:

多线程编程的一点小心得(2)

可以看到有两个“4”分别出现在了线程1跟线程2中,这显然是我们不想看到的。

有很多方法可以用来解决这些问题,今天来一一学习。

第一个是比较简单的方法:互斥锁。看到“锁”其实已经明白一半了,就是线程1在使用这个变量的时候就把这个变量“锁”起来,线程2只能在线程1把“锁”打开的时候才能使用,这样就不会出现上面那种糟糕的情况了。

互斥锁的使用方法也很简单:

  
 
 
  1. #include "stdafx.h" 
  2. #include <iostream> 
  3. #include <Windows.h> 
  4.  
  5. int g_nCounter = 0; 
  6. HANDLE g_hMutex; 
  7.  
  8. UINT WINAPI ThreadProc1(LPVOID lpParameter) 
  9.     while(g_nCounter < 100) 
  10.     { 
  11.         WaitForSingleObject(g_hMutex, INFINITE);        // 等待互斥对象 
  12.         std::cout << "gCounter in thread 1: " << g_nCounter++ << std::endl; 
  13.         ReleaseMutex(g_hMutex);                         // 释放对互斥对象的所有权 
  14.     } 
  15.     return 0; 
  16.  
  17. UINT WINAPI ThreadProc2(LPVOID lpParameter) 
  18.     while(g_nCounter < 100) 
  19.     { 
  20.         WaitForSingleObject(g_hMutex, INFINITE);        // 等待互斥对象 
  21.         std::cout << "gCounter in thread 2: " << g_nCounter++ << std::endl; 
  22.         ReleaseMutex(g_hMutex);                         // 释放对互斥对象的所有权 
  23.     } 
  24.     return 0; 
  25.  
  26. int main(int argc, _TCHAR* argv[]) 
  27.     HANDLE hThread1; 
  28.     HANDLE hThread2; 
  29.  
  30.     g_hMutex = CreateMutex(NULL, FALSE, _T("Mutex"));   // 创建互斥对象 
  31.  
  32.     hThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc1, NULL, 0, NULL); 
  33.     hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc2, NULL, 0, NULL); 
  34.  
  35.     Sleep(2000); 
  36.  
  37.     CloseHandle(hThread1); 
  38.     CloseHandle(hThread2); 
  39.     return 0; 

这个结果看起来就比较讨喜了:

多线程编程的一点小心得(2)

第二个方法:信号量。信号量采用了使用计数(让我想起了ios =。=)的机制,当使用计数为0时,表示无信号;大于0时表示有信号。有两个函数可以用来操作信号量:

  
 
 
  1. HANDLE WINAPI CreateSemaphore( 
  2.   __in_opt  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, 
  3.   __in      LONG lInitialCount, 
  4.   __in      LONG lMaximumCount, 
  5.   __in_opt  LPCTSTR lpName 
  6. ); 
  7.  
  8. BOOL WINAPI ReleaseSemaphore( 
  9.   __in       HANDLE hSemaphore, 
  10.   __in       LONG lReleaseCount, 
  11.   __out_opt  LPLONG lpPreviousCount 
  12. ); 

CreateSemaphore中的lInitialCount就是初始计数,lMaximunCount就是最大计数。使用方法与互斥锁如出一辙:(不上全部代码了)

  
 
 
  1. HANDLE g_hSemaphore; 
  2. g_hSemaphore = CreateSemaphore(NULL, 1, 100, _T("Semaphore")); 
  3. // 线程中 
  4. WaitForSingleObject(g_hSemaphore, INFINITE);   
  5. // 操作完成后 
  6. ReleaseSemaphore(g_hSemaphore, 1, 0); 

第三个:事件。事件对象属于内核对象之一,可以分为两类:人工重置事件对象和自动重置事件对象。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。而当一个自动重置事件得到通知时,等待该事件的所有线程中只有一个线程变以可调度线程。它的可调用函数有下面几个:

  
 
 
  1. // 创建事件对象 
  2. HANDLE WINAPI CreateEvent( 
  3.   __in_opt  LPSECURITY_ATTRIBUTES lpEventAttributes, 
  4.   __in      BOOL bManualReset, 
  5.   __in      BOOL bInitialState, 
  6.   __in_opt  LPCTSTR lpName 
  7. ); 
  8. // 设置事件为通知状态 
  9. BOOL WINAPI SetEvent( 
  10.   __in  HANDLE hEvent 
  11. ); 
  12. // 设置事件为未通知状态 
  13. BOOL WINAPI ResetEvent( 
  14.   __in  HANDLE hEvent 
  15. ); 

使用方法: 

  
 
 
  1. HANDLE g_hEvent; // 创建事件 
  2. g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); 
  3. // 处理完一些事件后(比如说加载完文件)就设置事件 
  4. SetEvent(g_hEvent); 
  5. // 线程中 
  6. WaitForSingleObject(g_hEvent, INFINITE); 
  7. // 操作完成后 
  8. SetEvent(g_hEvent); 

最后一个:临界区。初听这个名字的时候觉得很吓人,老让我想起切尔诺贝利之类的事情。。开个玩笑。临界区又叫关键代码段,表示这部分关键代码要独占一部分资源。有几个函数可以用来操作临界区:

  
 
 
  1. // 初始化临界区 
  2. void WINAPI InitializeCriticalSection( 
  3.   __out  LPCRITICAL_SECTION lpCriticalSection 
  4. ); 
  5. // 进入临界区 
  6. void WINAPI EnterCriticalSection( 
  7.   __inout  LPCRITICAL_SECTION lpCriticalSection 
  8. ); 
  9. // 离开临界区 
  10. void WINAPI LeaveCriticalSection( 
  11.   __inout  LPCRITICAL_SECTION lpCriticalSection 
  12. ); 
  13. // 释放临界区 
  14. void WINAPI DeleteCriticalSection( 
  15.   __inout  LPCRITICAL_SECTION lpCriticalSection 
  16. ); 

使用方法:

  
 
 
  1. CRITICAL_SECTION g_Critical;  
  2. // 线程中  
  3. EnterCriticalSection(&g_Critical);  
  4. // 操作完成后  
  5. LeaveCriticalSection(&g_Critical);  
  6. // 最后释放临界区  
  7. DeleteCriticalSection(&g_Critical);  

但是使用临界区的时候貌似出了点意外:主线程与子线程之间的同步不复存在了。

多线程编程的一点小心得(2)

这是咋回事咧?是操作不当吗?为此我也寻找了一些资料,在这儿找到了答案。http://blog.csdn.net/morewindows/article/details/7442639

下面的一段总结比较不错:

 

因此可以将关键段比作旅馆的房卡,调用EnterCriticalSection()即申请房卡,得到房卡后自己当然是可以多次进出房间的,在你调用LeaveCriticalSection()交出房卡之前,别人自然是无法进入该房间。

回到这个经典线程同步问题上,主线程正是由于拥有“线程所有权”即房卡,所以它可以重复进入关键代码区域从而导致子线程在接收参数之前主线程就已经修改了这个参数。所以关键段可以用于线程间的互斥,但不可以用于同步。

 

最后来说说这四种方法都在什么时候用吧,看起来似乎都差不多的样子。。

互斥多用在被多个线程所访问的内存块的保护上,可以确保任何线程在处理此内存块时都对其拥有独占访问权。

信号量主要是为控制一个具有有限数量用户资源而设计的,比如将lMaximumCount设为10,那么一次只能最多有10个线程(12306.cn多线程编程的一点小心得(2))来对资源进行访问。

当一个线程执行初始化,并通知另一个线程进行其他操作时, 事件使用得最多。比如我要写一个程序,当文件全部读入到缓存中的时候,对文件进行字数统计和字数查检,这个时候用事件。

临界区是确保资源在同一时刻只能被同一个线程访问的简便方法,如果有其他线程尝试访问该临界区,则会被挂起,等退出临界区后其他线程可以继续抢占。

这几种方法的效率呢?临界区是最高的,因为它没有进入内核级别,而其他的三种方法都属于内核对象,虽然效率稍微低一些,但是可操作性比较强。

本文出自 “正面旺得福反面泰瑞宝” 博客,请务必保留此出处http://serious.blog.51cto.com/242085/858398