VC中多线程同步技术

考虑到内容的连贯性,我对几乎重写了这篇博客,在这一小节,主要介绍线程以及线程间的同步,而把那个聊天工具放到下一节。

什么是程序?程序是计算计指令的集合,它以文件的形式储存在磁盘上。
什么是进程?进程是一个正在运行的程序的实例,是一个程序在其自身的地址空间内中的一次执行活动。因此,一个程序可以对应多个进程,比如我们可以把自己编写的简单的“hello, world”程序执行很多遍。
进程是资源申请、调度很运行的基本单位,因此:


[cpp] view plaincopy

  1. #include <stdio.h>  

  2.   

  3. int a = 0;  

  4.   

  5. int main()  

  6. {  

  7.     printf("%d",a);  

  8.     ++a;  

  9.     return 0;  

  10. }  


尽管a是“全局”变量,但假如我们把这个程序执行2遍,每次打印出来的结果都是0。
在Windows系统下,进程有两部分组成:
(1)操作系统用来管理进程的内核对象。
这些对象用来存放进程统计信息。他们是操作系统内部分配的内存块,只能被内核访问使用,用用程序无法找到该数据结构,并直接改变其内容。Windows提供了一些函数来对内核对象进行操作。
(2)地址空间
它包含所有可执行的模块或DLL模块的代码和数据,也包含动态分配的空间。例如线程的栈和堆。
关于进程的知识我们会在后面的章节仔细讲解。


实际上,进程从来不执行任何东西,若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。也就是说,真正完成代码执行的是线程,而进程只是线程的容器。线程是使用系统资源的基本单位。


线程也由两部分组成:
(1)线程的内核对象。操作系统用它来对线程进行管理,存放线程的统计信息。
(2)线程栈。它用于维护线程执行代码时所需要的所有函数和参数的局部变量。


线程只有一个内核对象和一个栈,开销相对较少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。
与线程相关的基本函数包括:
CreateThread:创建线程
CloseHandle:关闭线程句柄。注意,这只会使指定的线程句柄无效(减少该句柄的引用计数),启动句柄的检查操作,如果一个对象所关联的最后一个句柄被关闭了,那么这个对象会从系统中被删除。关闭句柄不会终止相关的线程。


线程是如何运行的呢?这又与你的CPU有关系了,如果你是一个单核CPU,那么系统会采用时间片轮询的方式运行每个线程;如果你是多核CPU,那么线程之间就有可能并发运行了。这样就会出现很多问题,比如两个线程同时访问一个全局变量之类的。它们需要线程的同步来解决。所谓同步,并不是多个线程一起同时执行,而是他们协同步调,按预定的先后次序执行。
Windows下线程同步的基本方法有3种:互斥对象、事件对象、关键代码段(临界区),下面一一介绍:

互斥对象属于内核对象,包含3个成员:
1.使用数量:记录了有多少个线程在调用该对象
2.一个线程ID:记录互斥对象维护的线程的ID
3.一个计数器:前线程调用该对象的次数
与之相关的函数包括:
创建互斥对象:CreateMutex
判断能否获得互斥对象:WaitForSingleObject
对于WaitForSingleObject,如果互斥对象为有信号状态,则获取成功,函数将互斥对象设置为无信号状态,程序将继续往下执行;如果互斥对象为无信号状态,则获取失败,线程会停留在这里等待。等待的时间可以由参数控制。
释放互斥对象:ReleaseMutex
当要保护的代码执行完毕后,通过它来释放互斥对象,使得互斥对象变为有信号状态,以便于其他线程可以获取这个互斥对象。注意,只有当某个线程拥有互斥对象时,才能够释放互斥对象,在其他线程调用这个函数不得达到释放的效果,这可以通过互斥对象的线程ID来判断。


我们看一个例子:



[cpp] view plaincopy

  1. #include <Windows.h>  

  2. #include <stdio.h>  

  3.   

  4. //线程函数声明  

  5. DWORD WINAPI Thread1Proc(  LPVOID lpParameter);  

  6. DWORD WINAPI Thread2Proc(  LPVOID lpParameter);  

  7.   

  8. //全局变量  

  9. int tickets = 100;  

  10. HANDLE hMutex;  

  11.   

  12. int main()  

  13. {  

  14.     HANDLE hThread1;  

  15.     HANDLE hThread2;  

  16.     //创建互斥对象  

  17.     hMutex = CreateMutex( NULL,         //默认安全级别  

  18.                           FALSE,        //创建它的线程不拥有互斥对象  

  19.                           NULL);        //没有名字  

  20.     //创建线程1  

  21.     hThread1 = CreateThread(NULL,       //默认安全级别  

  22.                             0,          //默认栈大小  

  23.                             Thread1Proc,//线程函数   

  24.                             NULL,       //函数没有参数  

  25.                             0,          //创建后直接运行  

  26.                             NULL);      //线程标识,不需要  

  27.   

  28.     //创建线程2  

  29.     hThread2 = CreateThread(NULL,       //默认安全级别  

  30.                             0,          //默认栈大小  

  31.                             Thread2Proc,//线程函数   

  32.                             NULL,       //函数没有参数  

  33.                             0,          //创建后直接运行  

  34.                             NULL);      //线程标识,不需要  

  35.   

  36.     //主线程休眠4秒  

  37.     Sleep(4000);  

  38.     //主线程休眠4秒  

  39.     Sleep(4000);  

  40.     //关闭线程句柄  

  41.     CloseHandle(hThread1);  

  42.     CloseHandle(hThread2);  

  43.   

  44.     //释放互斥对象  

  45.     ReleaseMutex(hMutex);  

  46.     return 0;  

  47. }  

  48.   

  49. //线程1入口函数  

  50. DWORD WINAPI Thread1Proc(  LPVOID lpParameter)  

  51. {  

  52.     while(TRUE)  

  53.     {  

  54.         WaitForSingleObject(hMutex,INFINITE);  

  55.         if(tickets > 0)  

  56.         {  

  57.             Sleep(10);  

  58.             printf("thread1 sell ticket : %d\n",tickets--);  

  59.             ReleaseMutex(hMutex);  

  60.         }  

  61.         else  

  62.         {  

  63.             ReleaseMutex(hMutex);  

  64.             break;  

  65.         }  

  66.     }  

  67.   

  68.     return 0;  

  69. }  

  70.   

  71. //线程2入口函数  

  72. DWORD WINAPI Thread2Proc(  LPVOID lpParameter)  

  73. {  

  74.     while(TRUE)  

  75.     {  

  76.         WaitForSingleObject(hMutex,INFINITE);  

  77.         if(tickets > 0)  

  78.         {  

  79.             Sleep(10);  

  80.             printf("thread2 sell ticket : %d\n",tickets--);  

  81.             ReleaseMutex(hMutex);  

  82.         }  

  83.         else  

  84.         {  

  85.             ReleaseMutex(hMutex);  

  86.             break;  

  87.         }  

  88.     }  

  89.   

  90.     return 0;  

  91. }  

使用互斥对象时需要小心:
调用假如一个线程本身已经拥有该互斥对象,则如果它继续调用WaitForSingleObject,则会增加互斥对象的引用计数,此时,你必须多次调用ReleaseMutex来释放互斥对象,以便让其他线程可以获取:



[cpp] view plaincopy

  1. //创建互斥对象  

  2. hMutex = CreateMutex( NULL,         //默认安全级别  

  3.                       TRUE,         //创建它的线程拥有互斥对象  

  4.                       NULL);        //没有名字  

  5. WaitForSingleObject(hMutex,INFINITE);  

  6. //释放互斥对象  

  7. ReleaseMutex(hMutex);  

  8. //释放互斥对象  

  9. ReleaseMutex(hMutex);  

下面看事件对象,它也属于内核对象,包含3各成员:
1.使用计数
2.用于指明该事件是自动重置事件还是人工重置事件的布尔值
3.用于指明该事件处于已通知状态还是未通知状态。
自动重置和人工重置的事件对象有一个重要的区别:当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。
与事件对象相关的函数包括:
创建事件对象:CreateEvent
HANDLE CreateEvent(  LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState,LPCTSTR lpName);
设置事件对象:SetEvent:将一个这件对象设为有信号状态
BOOL SetEvent(  HANDLE hEvent  );
重置事件对象状态:ResetEvent:将指定的事件对象设为无信号状态
BOOL ResetEvent(  HANDLE hEvent );

下面仍然使用买火车票的例子:



[cpp] view plaincopy

  1. #include <Windows.h>  

  2. #include <stdio.h>  

  3.   

  4. //线程函数声明  

  5. DWORD WINAPI Thread1Proc(  LPVOID lpParameter);  

  6. DWORD WINAPI Thread2Proc(  LPVOID lpParameter);  

  7.   

  8. //全局变量  

  9. int tickets = 100;  

  10. HANDLE g_hEvent;  

  11.   

  12. int main()  

  13. {  

  14.     HANDLE hThread1;  

  15.     HANDLE hThread2;  

  16.     //创建事件对象  

  17.     g_hEvent = CreateEvent( NULL,   //默认安全级别  

  18.                             TRUE,   //人工重置  

  19.                             FALSE,  //初始为无信号  

  20.                             NULL ); //没有名字  

  21.     //创建线程1  

  22.     hThread1 = CreateThread(NULL,       //默认安全级别  

  23.                             0,          //默认栈大小  

  24.                             Thread1Proc,//线程函数   

  25.                             NULL,       //函数没有参数  

  26.                             0,          //创建后直接运行  

  27.                             NULL);      //线程标识,不需要  

  28.   

  29.     //创建线程2  

  30.     hThread2 = CreateThread(NULL,       //默认安全级别  

  31.                             0,          //默认栈大小  

  32.                             Thread2Proc,//线程函数   

  33.                             NULL,       //函数没有参数  

  34.                             0,          //创建后直接运行  

  35.                             NULL);      //线程标识,不需要  

  36.   

  37.   

  38.     //主线程休眠4秒  

  39.     Sleep(4000);  

  40.     //关闭线程句柄  

  41.     //当不再引用这个句柄时,立即将其关闭,减少其引用计数  

  42.     CloseHandle(hThread1);  

  43.     CloseHandle(hThread2);  

  44.     //关闭事件对象句柄  

  45.     CloseHandle(g_hEvent);  

  46.     return 0;  

  47. }  

  48.   

  49. //线程1入口函数  

  50. DWORD WINAPI Thread1Proc(  LPVOID lpParameter)  

  51. {  

  52.     while(TRUE)  

  53.     {  

  54.         WaitForSingleObject(g_hEvent,INFINITE);  

  55.         if(tickets > 0)  

  56.         {  

  57.             Sleep(1);  

  58.             printf("thread1 sell ticket : %d\n",tickets--);  

  59.         }  

  60.         else  

  61.             break;  

  62.     }  

  63.   

  64.     return 0;  

  65. }  

  66.   

  67. //线程2入口函数  

  68. DWORD WINAPI Thread2Proc(  LPVOID lpParameter)  

  69. {  

  70.     while(TRUE)  

  71.     {  

  72.         WaitForSingleObject(g_hEvent,INFINITE);  

  73.         if(tickets > 0)  

  74.         {  

  75.             Sleep(1);  

  76.             printf("thread2 sell ticket : %d\n",tickets--);  

  77.         }  

  78.         else  

  79.             break;  

  80.     }  

  81.   

  82.     return 0;  

  83. }  

程序运行后并没有出现两个线程买票的情况,而是等待了4秒之后直接退出了,这是什么原因呢?问题出在了我们创建的事件对象一开始就是无信号状态的,因此2个线程线程运行到WaitForSingleObject时就会一直等待,直到自己的时间片结束。所以什么也不会输出。
如果想让线程能够执行,可以在创建线程时将第3个参数设为TRUE,或者在创建完成后调用



[cpp] view plaincopy

  1. SetEvent(g_hEvent);  

程序的确可以实现买票了,但是有些时候,会打印出某个线程卖出第0张票的情况,这是因为当人工重置的事件对象得到通知时,等待该对象的所有线程均可变为可调度线程,两个线程同时运行,线程的同步失败了。


也许有人会想到,在线程得到CPU之后,能否使用ResetEvent是得线程将事件对象设为无信号状态,然后当所保护的代码运行完成后,再将事件对象设为有信号状态?我们可以试试:



[cpp] view plaincopy

  1. //线程1入口函数  

  2. DWORD WINAPI Thread1Proc(  LPVOID lpParameter)  

  3. {  

  4.     while(TRUE)  

  5.     {  

  6.         WaitForSingleObject(g_hEvent,INFINITE);  

  7.         ResetEvent(g_hEvent);  

  8.         if(tickets > 0)  

  9.         {  

  10.             Sleep(10);  

  11.             printf("thread1 sell ticket : %d\n",tickets--);  

  12.             SetEvent(g_hEvent);  

  13.         }  

  14.         else  

  15.         {  

  16.             SetEvent(g_hEvent);  

  17.             break;  

  18.         }  

  19.     }  

  20.   

  21.     return 0;  

  22. }  

线程2的类似,这里就省略了。运行程序,发现依然会出现卖出第0张票的情况。这是为什么呢?我们仔细思考一下:单核CPU下,可能线程1执行完WaitForSingleObject,还没来得及执行ResetEvent时,就切换到线程2了,此时,由于线程1并没有执行ResetEvent,所以线程2也可以得到事件对象了。而在多CPU平台下,假如两个线程同时执行,则有可能都执行到本应被保护的代码区域。
所以,为了实现线程间的同步,不应该使用人工重置的事件对象,而应该使用自动重置的事件对象:



[cpp] view plaincopy

  1. hThread2 = CreateThread(NULL,0,Thread2Proc,NULL0,NULL);  

并将原来写的ResetEvent和SetEvent全都注释起来。我们发现程序只打印了一次买票过程。我们分析一下原因:
当一个自动重置的事件得到通知后,等待该该事件的线程中只有一个变为可调度线程。在这里,线程1变为可调度线程后,操作系统将事件设为了无信号状态,当线程1休眠时,所以线程2只能等待,时间片结束以后,又轮到线程1运行,输出thread1 sell ticket :100。然后循环,又去WaitForSingleObject,而此时事件对象处于无信号状态,所以线程不能继续往下执行,只能一直等待,等到自己时间片结束,直到主线程睡醒了,结束整个程序。
正确的使用方法是:当访问完对保护的代码段后,立即调用SetEvent将其设为有信号状态。允许其他等待该对象的线程变为可调度状态:



[cpp] view plaincopy

  1. DWORD WINAPI Thread1Proc(  LPVOID lpParameter)  

  2. {  

  3.     while(TRUE)  

  4.     {  

  5.         WaitForSingleObject(g_hEvent,INFINITE);  

  6.         if(tickets > 0)  

  7.         {  

  8.             Sleep(10);  

  9.             printf("thread1 sell ticket : %d\n",tickets--);  

  10.             SetEvent(g_hEvent);  

  11.         }  

  12.         else  

  13.         {  

  14.             SetEvent(g_hEvent);  

  15.             break;  

  16.         }  

  17.     }  

  18.   

  19.     return 0;  

  20. }  

总结一下:事件对象要区分人工重置事件还是自动重置事件。如果是人工重置的事件对象得到通知,则等待该事件对象的所有线程均变为可调度线程;当一个自动重置的事件对象得到通知时,只有一个等待该事件对象的线程变为可调度线程,且操作系统会将该事件对象设为无信号状态。因此,当执行完受保护的代码后,需要调用SetEvent将事件对象设为有信号状态。


下面介绍另一种线程同步的方法:关键代码段。
关键代码段又称为临界区,工作在用户方式下。它是一小段代码,但是在代码执行之前,必须独占某些资源的访问权限。
我们先介绍与之先关的API函数:
使用InitializeCriticalSection初始化关键代码段
使用EnterCriticalSection进入关键代码段:
使用LeaveCriticalSection离开关键代码段:
使用DeleteCriticalSection删除关键代码段,释放资源
我们看一个例子:



[cpp] view plaincopy

  1. #include <Windows.h>  

  2. #include <stdio.h>  

  3.   

  4. //线程函数声明  

  5. DWORD WINAPI Thread1Proc(  LPVOID lpParameter);  

  6. DWORD WINAPI Thread2Proc(  LPVOID lpParameter);  

  7.   

  8. //全局变量  

  9. int tickets = 100;  

  10. CRITICAL_SECTION g_cs;  

  11.   

  12. int main()  

  13. {  

  14.     HANDLE hThread1;  

  15.     HANDLE hThread2;  

  16.     //初始化关键代码段  

  17.     InitializeCriticalSection(&g_cs);  

  18.     //创建线程1  

  19.     hThread1 = CreateThread(NULL,       //默认安全级别  

  20.                             0,          //默认栈大小  

  21.                             Thread1Proc,//线程函数   

  22.                             NULL,       //函数没有参数  

  23.                             0,          //创建后直接运行  

  24.                             NULL);      //线程标识,不需要  

  25.   

  26.     //创建线程2  

  27.     hThread2 = CreateThread(NULL,       //默认安全级别  

  28.                             0,          //默认栈大小  

  29.                             Thread2Proc,//线程函数   

  30.                             NULL,       //函数没有参数  

  31.                             0,          //创建后直接运行  

  32.                             NULL);      //线程标识,不需要  

  33.   

  34.   

  35.     //主线程休眠4秒  

  36.     Sleep(4000);  

  37.     //关闭线程句柄  

  38.     CloseHandle(hThread1);  

  39.     CloseHandle(hThread2);  

  40.     //关闭事件对象句柄  

  41.     DeleteCriticalSection(&g_cs);  

  42.     return 0;  

  43. }  

  44.   

  45. //线程1入口函数  

  46. DWORD WINAPI Thread1Proc(  LPVOID lpParameter)  

  47. {  

  48.     while(TRUE)  

  49.     {  

  50.         //进入关键代码段前调用该函数判断否能得到临界区的使用权  

  51.         EnterCriticalSection(&g_cs);  

  52.         Sleep(1);  

  53.         if(tickets > 0)  

  54.         {  

  55.             Sleep(1);  

  56.             printf("thread1 sell ticket : %d\n",tickets--);  

  57.             //访问结束后释放临界区对象的使用权  

  58.             LeaveCriticalSection(&g_cs);  

  59.             Sleep(1);  

  60.         }  

  61.         else  

  62.         {  

  63.             LeaveCriticalSection(&g_cs);  

  64.             break;  

  65.         }  

  66.     }  

  67.   

  68.     return 0;  

  69. }  

  70.   

  71. //线程2入口函数  

  72. DWORD WINAPI Thread2Proc(  LPVOID lpParameter)  

  73. {  

  74.     while(TRUE)  

  75.     {  

  76.         //进入关键代码段前调用该函数判断否能得到临界区的使用权  

  77.         EnterCriticalSection(&g_cs);  

  78.         Sleep(1);  

  79.         if(tickets > 0)  

  80.         {  

  81.             Sleep(1);  

  82.             printf("thread2 sell ticket : %d\n",tickets--);  

  83.             //访问结束后释放临界区对象的使用权  

  84.             LeaveCriticalSection(&g_cs);  

  85.             Sleep(1);  

  86.         }  

  87.         else  

  88.         {  

  89.             LeaveCriticalSection(&g_cs);  

  90.             break;  

  91.         }  

  92.     }  

  93.   

  94.     return 0;  

  95. }  

在这个例子中,通过在放弃临界区资源后,立即睡眠引起另一个线程被调用,导致两个线程交替售票。
下面看一个多线程程序中常犯的一个错误-线程死锁。死锁产生的原因,举个例子:线程1拥有临界区资源A,正在等待临界区资源B;而线程2拥有临界区资源B,正在等待临界区资源A。它俩各不相让,结果谁也执行不了。我们看看程序:



[cpp] view plaincopy

  1. #include <Windows.h>  

  2. #include <stdio.h>  

  3.   

  4. //线程函数声明  

  5. DWORD WINAPI Thread1Proc(  LPVOID lpParameter);  

  6. DWORD WINAPI Thread2Proc(  LPVOID lpParameter);  

  7.   

  8. //全局变量  

  9. int tickets = 100;  

  10. CRITICAL_SECTION g_csA;  

  11. CRITICAL_SECTION g_csB;  

  12. int main()  

  13. {  

  14.     HANDLE hThread1;  

  15.     HANDLE hThread2;  

  16.     //初始化关键代码段  

  17.     InitializeCriticalSection(&g_csA);  

  18.     InitializeCriticalSection(&g_csB);  

  19.     //创建线程1  

  20.     hThread1 = CreateThread(NULL,       //默认安全级别  

  21.                             0,          //默认栈大小  

  22.                             Thread1Proc,//线程函数   

  23.                             NULL,       //函数没有参数  

  24.                             0,          //创建后直接运行  

  25.                             NULL);      //线程标识,不需要  

  26.   

  27.     //创建线程2  

  28.     hThread2 = CreateThread(NULL,       //默认安全级别  

  29.                             0,          //默认栈大小  

  30.                             Thread2Proc,//线程函数   

  31.                             NULL,       //函数没有参数  

  32.                             0,          //创建后直接运行  

  33.                             NULL);      //线程标识,不需要  

  34.     //关闭线程句柄  

  35.     //当不再引用这个句柄时,立即将其关闭,减少其引用计数  

  36.     CloseHandle(hThread1);  

  37.     CloseHandle(hThread2);  

  38.   

  39.     //主线程休眠4秒  

  40.     Sleep(4000);  

  41.   

  42.     //关闭事件对象句柄  

  43.     DeleteCriticalSection(&g_csA);  

  44.     DeleteCriticalSection(&g_csB);  

  45.     return 0;  

  46. }  

  47.   

  48. //线程1入口函数  

  49. DWORD WINAPI Thread1Proc(  LPVOID lpParameter)  

  50. {  

  51.     while(TRUE)  

  52.     {  

  53.         EnterCriticalSection(&g_csA);  

  54.         Sleep(1);  

  55.         EnterCriticalSection(&g_csB);  

  56.         if(tickets > 0)  

  57.         {  

  58.             Sleep(1);  

  59.             printf("thread1 sell ticket : %d\n",tickets--);  

  60.             LeaveCriticalSection(&g_csB);  

  61.             LeaveCriticalSection(&g_csA);  

  62.             Sleep(1);  

  63.         }  

  64.         else  

  65.         {  

  66.             LeaveCriticalSection(&g_csB);  

  67.             LeaveCriticalSection(&g_csA);  

  68.             break;  

  69.         }  

  70.     }  

  71.   

  72.     return 0;  

  73. }  

  74.   

  75. //线程2入口函数  

  76. DWORD WINAPI Thread2Proc(  LPVOID lpParameter)  

  77. {  

  78.     while(TRUE)  

  79.     {  

  80.         EnterCriticalSection(&g_csB);  

  81.         Sleep(1);  

  82.         EnterCriticalSection(&g_csA);  

  83.         if(tickets > 0)  

  84.         {  

  85.             Sleep(1);  

  86.             printf("thread2 sell ticket : %d\n",tickets--);  

  87.             LeaveCriticalSection(&g_csA);  

  88.             LeaveCriticalSection(&g_csB);  

  89.             Sleep(1);  

  90.         }  

  91.         else  

  92.         {  

  93.             LeaveCriticalSection(&g_csA);  

  94.             LeaveCriticalSection(&g_csB);  

  95.             break;  

  96.         }  

  97.     }  

  98.   

  99.     return 0;  

  100. }  


在程序中,创建了两个临界区对象g_csA和g_csB。线程1中先尝试获取g_csA,获取成功后休眠,线程2尝试获取g_csB,成功后休眠,切换回线程1,然后线程1试图获取g_csB,因为g_csB已经被线程2获取,所以它线程1的获取不会成功,一直等待,直到自己的时间片结束后,转到线程2,线程2获取g_csB后,试图获取g_csA,当然也不会成功,转回线程1……这样交替等待,直到主线程睡醒,执行完毕,程序结束。


你可能感兴趣的:(多线程,windows,return,include,聊天工具)