C++编程——多线程

一、几个重要函数

1、WaitForSingleObject()

2、CreateThread/_beginthreadex

3、CreateEvent

4、SetEvent

二、概念

1、线程、进程

 

说到多线程编程,那么就不得不提并行和并发,多线程是实现并发(并行)的一种手段。并行是指两个或多个独立的操作同时进行。注意这里是同时进行,区别于并发,在一个时间段内执行多个操作。在单核时代,多个线程是并发的,在一个时间段内轮流执行;在多核时代,多个线程可以实现真正的并行,在多核上真正独立的并行执行。例如现在常见的4核4线程可以并行4个线程;4核8线程则使用了超线程技术,把一个物理核模拟为2个逻辑核心,可以并行8个线程。

并发编程的方法

通常,要实现并发有两种方法:多进程和多线程。

多进程并发

使用多进程并发是将一个应用程序划分为多个独立的进程(每个进程只有一个线程),这些独立的进程间可以互相通信,共同完成任务。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但这也造就了多进程并发的两个缺点:

  • 在进程件的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。
  • 运行多个线程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。

由于多个进程并发完成同一个任务时,不可避免的是:操作同一个数据和进程间的相互通信,上述的两个缺点也就决定了多进程的并发不是一个好的选择。

多线程并发

多线程并发指的是在同一个进程中执行多个线程。有操作系统相关知识的应该知道,线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。

 

以下内容均为转载博主MoreWindows多线程系列专题文章,仅供学习,文章地址为:http://blog.csdn.net/morewindows/article/details/7421759

 

转载第一部分

多线程第一次亲密接触 CreateThread与_beginthreadex本质区别】

本文将带领你与多线程作第一次亲密接触,并深入分析CreateThread_beginthreadex的本质区别,相信阅读本文后你能轻松的使用多线程并能流畅准确的回答CreateThread_beginthreadex到底有什么区别,在实际的编程中到底应该使用CreateThread还是_beginthreadex

 

   使用多线程其实是非常容易的,下面这个程序的主线程会创建了一个子线程并等待其运行完毕,子线程就输出它的线程ID号然后输出一句经典名言——Hello World。整个程序的代码非常简短,只有区区几行。

//最简单的创建多线程实例  
#include <stdio.h>  
#include <windows.h>  
//子线程函数  
DWORD WINAPI ThreadFun(LPVOID pM)  
{  
    printf("子线程的线程ID号为:%d\n子线程输出Hello World\n", GetCurrentThreadId());  
    return 0;  
}  
//主函数,所谓主函数其实就是主线程执行的函数。  
int main()  
{  
    printf("     最简单的创建多线程实例\n");  
    printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  
    HANDLE handle = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL);  
    WaitForSingleObject(handle, INFINITE);  
    return 0;  
}  
 
 

 

运行结果如下所示:

 

 

下面来细讲下代码中的一些函数

第一个 CreateThread

函数功能:创建线程

函数原型:

HANDLEWINAPICreateThread(

  LPSECURITY_ATTRIBUTESlpThreadAttributes,

  SIZE_TdwStackSize,

  LPTHREAD_START_ROUTINElpStartAddress,

  LPVOIDlpParameter,

  DWORDdwCreationFlags,

  LPDWORDlpThreadId

);

函数说明:

第一个参数表示线程内核对象的安全属性,一般传入NULL表示使用默认设置。

第二个参数表示线程栈空间大小。传入0表示使用默认大小(1MB)。

第三个参数表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。

第四个参数是传给线程函数的参数。

第五个参数指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()

第六个参数将返回线程的ID号,传入NULL表示不需要返回该线程ID号。

函数返回值:

成功返回新线程的句柄,失败返回NULL。 

 

第二个 WaitForSingleObject

函数功能:等待函数 – 使线程进入等待状态,直到指定的内核对象被触发。

函数原形:

DWORDWINAPIWaitForSingleObject(

  HANDLEhHandle,

  DWORDdwMilliseconds

);

函数说明:

第一个参数为要等待的内核对象。

第二个参数为最长等待的时间,以毫秒为单位,如传入5000就表示5秒,传入0就立即返回,传入INFINITE表示无限等待。

因为线程的句柄在线程运行时是未触发的,线程结束运行,句柄处于触发状态。所以可以用WaitForSingleObject()来等待一个线程结束运行。

函数返回值:

在指定的时间内对象被触发,函数返回WAIT_OBJECT_0。超过最长等待时间对象仍未被触发返回WAIT_TIMEOUT。传入参数有错误将返回WAIT_FAILED

 

CreateThread()函数是Windows提供的API接口,在C/C++语言另有一个创建线程的函数_beginthreadex(),在很多书上(包括《Windows核心编程》)提到过尽量使用_beginthreadex()来代替使用CreateThread(),这是为什么了?下面就来探索与发现它们的区别吧。

 

       首先要从标准C运行库与多线程的矛盾说起,标准C运行库在1970年被实现了,由于当时没任何一个操作系统提供对多线程的支持。因此编写标准C运行库的程序员根本没考虑多线程程序使用标准C运行库的情况。比如标准C运行库的全局变量errno。很多运行库中的函数在出错时会将错误代号赋值给这个全局变量,这样可以方便调试。但如果有这样的一个代码片段:

[cpp]  view plain  copy
 
  1. if (system("notepad.exe readme.txt") == -1)  
  2. {  
  3.     switch(errno)  
  4.     {  
  5.         ...//错误处理代码  
  6.     }  
  7. }  

假设某个线程A在执行上面的代码,该线程在调用system()之后且尚未调用switch()语句时另外一个线程B启动了,这个线程B也调用了标准C运行库的函数,不幸的是这个函数执行出错了并将错误代号写入全局变量errno中。这样线程A一旦开始执行switch()语句时,它将访问一个被B线程改动了的errno。这种情况必须要加以避免!因为不单单是这一个变量会出问题,其它像strerror()strtok()tmpnam()gmtime()asctime()等函数也会遇到这种由多个线程访问修改导致的数据覆盖问题。

 

为了解决这个问题,Windows操作系统提供了这样的一种解决方案——每个线程都将拥有自己专用的一块内存区域来供标准C运行库中所有有需要的函数使用。而且这块内存区域的创建就是由C/C++运行库函数_beginthreadex()来负责的。下面列出_beginthreadex()函数的源代码(我在这份代码中增加了一些注释)以便读者更好的理解_beginthreadex()函数与CreateThread()函数的区别。

[cpp]  view plain  copy
 
  1. //_beginthreadex源码整理By MoreWindows( http://blog.csdn.net/MoreWindows )  
  2. _MCRTIMP uintptr_t __cdecl _beginthreadex(  
  3.     void *security,  
  4.     unsigned stacksize,  
  5.     unsigned (__CLR_OR_STD_CALL * initialcode) (void *),  
  6.     void * argument,  
  7.     unsigned createflag,  
  8.     unsigned *thrdaddr  
  9. )  
  10. {  
  11.     _ptiddata ptd;          //pointer to per-thread data 见注1  
  12.     uintptr_t thdl;         //thread handle 线程句柄  
  13.     unsigned long err = 0L; //Return from GetLastError()  
  14.     unsigned dummyid;    //dummy returned thread ID 线程ID号  
  15.       
  16.     // validation section 检查initialcode是否为NULL  
  17.     _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0);  
  18.   
  19.     //Initialize FlsGetValue function pointer  
  20.     __set_flsgetvalue();  
  21.       
  22.     //Allocate and initialize a per-thread data structure for the to-be-created thread.  
  23.     //相当于new一个_tiddata结构,并赋给_ptiddata指针。  
  24.     if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )  
  25.         goto error_return;  
  26.   
  27.     // Initialize the per-thread data  
  28.     //初始化线程的_tiddata块即CRT数据区域 见注2  
  29.     _initptd(ptd, _getptd()->ptlocinfo);  
  30.       
  31.     //设置_tiddata结构中的其它数据,这样这块_tiddata块就与线程联系在一起了。  
  32.     ptd->_initaddr = (void *) initialcode; //线程函数地址  
  33.     ptd->_initarg = argument;              //传入的线程参数  
  34.     ptd->_thandle = (uintptr_t)(-1);  
  35.       
  36. #if defined (_M_CEE) || defined (MRTDLL)  
  37.     if(!_getdomain(&(ptd->__initDomain))) //见注3  
  38.     {  
  39.         goto error_return;  
  40.     }  
  41. #endif  // defined (_M_CEE) || defined (MRTDLL)  
  42.       
  43.     // Make sure non-NULL thrdaddr is passed to CreateThread  
  44.     if ( thrdaddr == NULL )//判断是否需要返回线程ID号  
  45.         thrdaddr = &dummyid;  
  46.   
  47.     // Create the new thread using the parameters supplied by the caller.  
  48.     //_beginthreadex()最终还是会调用CreateThread()来向系统申请创建线程  
  49.     if ( (thdl = (uintptr_t)CreateThread(  
  50.                     (LPSECURITY_ATTRIBUTES)security,  
  51.                     stacksize,  
  52.                     _threadstartex,  
  53.                     (LPVOID)ptd,  
  54.                     createflag,  
  55.                     (LPDWORD)thrdaddr))  
  56.         == (uintptr_t)0 )  
  57.     {  
  58.         err = GetLastError();  
  59.         goto error_return;  
  60.     }  
  61.   
  62.     //Good return  
  63.     return(thdl); //线程创建成功,返回新线程的句柄.  
  64.       
  65.     //Error return  
  66. error_return:  
  67.     //Either ptd is NULL, or it points to the no-longer-necessary block  
  68.     //calloc-ed for the _tiddata struct which should now be freed up.  
  69.     //回收由_calloc_crt()申请的_tiddata块  
  70.     _free_crt(ptd);  
  71.     // Map the error, if necessary.  
  72.     // Note: this routine returns 0 for failure, just like the Win32  
  73.     // API CreateThread, but _beginthread() returns -1 for failure.  
  74.     //校正错误代号(可以调用GetLastError()得到错误代号)  
  75.     if ( err != 0L )  
  76.         _dosmaperr(err);  
  77.     return( (uintptr_t)0 ); //返回值为NULL的效句柄  
  78. }  

讲解下部分代码:

注1._ptiddataptd;中的_ptiddata是个结构体指针。在mtdll.h文件被定义:

      typedefstruct_tiddata * _ptiddata

微软对它的注释为Structure for each thread's data这是一个非常大的结构体,有很多成员。本文由于篇幅所限就不列出来了。

 

2_initptd(ptd_getptd()->ptlocinfo);微软对这一句代码中的getptd()的说明为:

      /* return address of per-thread CRT data */

      _ptiddata __cdecl_getptd(void);

_initptd()说明如下:

      /* initialize a per-thread CRT data block */

      void__cdecl_initptd(_Inout_ _ptiddata _Ptd,_In_opt_ pthreadlocinfo _Locale);

注释中的CRT (C Runtime Library)即标准C运行库。

 

注3.if(!_getdomain(&(ptd->__initDomain)))中的_getdomain()函数代码可以在thread.c文件中找到,其主要功能是初始化COM环境。

由上面的源代码可知,_beginthreadex()函数在创建新线程时会分配并初始化一个_tiddata块。这个_tiddata块自然是用来存放一些需要线程独享的数据。事实上新线程运行时会首先将_tiddata块与自己进一步关联起来。然后新线程调用标准C运行库函数如strtok()时就会先取得_tiddata块的地址再将需要保护的数据存入_tiddata块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其它线程的数据了。因此,如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()相信阅读到这里时,你会对这句简短的话有个非常深刻的印象,如果有面试官问起,你也可以流畅准确的回答了^_^。

 

接下来,类似于上面的程序用CreateThread()创建输出“Hello World”的子线程,下面使用_beginthreadex()来创建多个子线程:

[cpp]  view plain  copy
 
  1. //创建多子个线程实例  
  2. #include <stdio.h>  
  3. #include <process.h>  
  4. #include <windows.h>  
  5. //子线程函数  
  6. unsigned int __stdcall ThreadFun(PVOID pM)  
  7. {  
  8.     printf("线程ID号为%4d的子线程说:Hello World\n", GetCurrentThreadId());  
  9.     return 0;  
  10. }  
  11. //主函数,所谓主函数其实就是主线程执行的函数。  
  12. int main()  
  13. {  
  14.     printf("     创建多个子线程实例 \n");  
  15.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  16.       
  17.     const int THREAD_NUM = 5;  
  18.     HANDLE handle[THREAD_NUM];  
  19.     for (int i = 0; i < THREAD_NUM; i++)  
  20.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);  
  21.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  22.     return 0;  
  23. }  

运行结果如下:

 

图中每个子线程说的都是同一句话,不太好看。能不能来一个线程报数功能,即第一个子线程输出1,第二个子线程输出2,第三个子线程输出3,……。要实现这个功能似乎非常简单——每个子线程对一个全局变量进行递增并输出就可以了。代码如下:

[cpp]  view plain  copy
 
  1. //子线程报数  
  2. #include <stdio.h>  
  3. #include <process.h>  
  4. #include <windows.h>  
  5. int g_nCount;  
  6. //子线程函数  
  7. unsigned int __stdcall ThreadFun(PVOID pM)  
  8. {  
  9.     g_nCount++;  
  10.     printf("线程ID号为%4d的子线程报数%d\n", GetCurrentThreadId(), g_nCount);  
  11.     return 0;  
  12. }  
  13. //主函数,所谓主函数其实就是主线程执行的函数。  
  14. int main()  
  15. {  
  16.     printf("     子线程报数 \n");  
  17.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  18.       
  19.     const int THREAD_NUM = 10;  
  20.     HANDLE handle[THREAD_NUM];  
  21.   
  22.     g_nCount = 0;  
  23.     for (int i = 0; i < THREAD_NUM; i++)  
  24.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);  
  25.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  26.     return 0;  
  27. }  

对一次运行结果截图如下:

 

 

显示结果从1数到10,看起来好象没有问题。

       答案是不对的,虽然这种做法在逻辑上是正确的,但在多线程环境下这样做是会产生严重的问题,下一篇《秒杀多线程第三篇 原子操作 Interlocked系列函数》将为你演示错误的结果(可能非常出人意料)并解释产生这个结果的详细原因。

 

转载第二部分

秒杀多线程第三篇 原子操作 Interlocked系列函数

 

 

上一篇《多线程第一次亲密接触 CreateThread与_beginthreadex本质区别》中讲到一个多线程报数功能。为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是否运行出错。这也非常类似于统计一个网站每天有多少用户登录,每个用户登录用一个线程模拟,线程运行时会将一个表示计数的变量递增。程序在最后输出计数的值表示有今天多少个用户登录,如果这个值不等于我们启动的线程个数,那显然说明这个程序是有问题的。整个程序代码如下:

[cpp]  view plain  copy
 
  1. #include <stdio.h>  
  2. #include <process.h>  
  3. #include <windows.h>  
  4. volatile long g_nLoginCount; //登录次数  
  5. unsigned int __stdcall Fun(void *pPM); //线程函数  
  6. const int THREAD_NUM = 10; //启动线程数  
  7. unsigned int __stdcall ThreadFun(void *pPM)  
  8. {  
  9.     Sleep(100); //some work should to do  
  10.     g_nLoginCount++;  
  11.     Sleep(50);   
  12.     return 0;  
  13. }  
  14. int main()  
  15. {  
  16.     g_nLoginCount = 0;  
  17.   
  18.     HANDLE  handle[THREAD_NUM];  
  19.     for (int i = 0; i < THREAD_NUM; i++)  
  20.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);  
  21.       
  22.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);   
  23.     printf("有%d个用户登录后记录结果是%d\n", THREAD_NUM, g_nLoginCount);  
  24.     return 0;  
  25. }  

程序中模拟的是10个用户登录,程序将输出结果:

 

 

上一篇的线程报数程序一样,程序输出的结果好象并没什么问题。下面我们增加点用户来试试,现在模拟50个用户登录,为了便于观察结果,在程序中将50个用户登录过程重复20次,代码如下:

[cpp]  view plain  copy
 
  1. #include <stdio.h>  
  2. #include <windows.h>  
  3. volatile long g_nLoginCount; //登录次数  
  4. unsigned int __stdcall Fun(void *pPM); //线程函数  
  5. const DWORD THREAD_NUM = 50;//启动线程数  
  6. DWORD WINAPI ThreadFun(void *pPM)  
  7. {  
  8.     Sleep(100); //some work should to do  
  9.     g_nLoginCount++;  
  10.     Sleep(50);  
  11.     return 0;  
  12. }  
  13. int main()  
  14. {  
  15.     printf("     原子操作 Interlocked系列函数的使用\n");  
  16.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  17.       
  18.     //重复20次以便观察多线程访问同一资源时导致的冲突  
  19.     int num= 20;  
  20.     while (num--)  
  21.     {     
  22.         g_nLoginCount = 0;  
  23.         int i;  
  24.         HANDLE  handle[THREAD_NUM];  
  25.         for (i = 0; i < THREAD_NUM; i++)  
  26.             handle[i] = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL);  
  27.         WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  28.         printf("有%d个用户登录后记录结果是%d\n", THREAD_NUM, g_nLoginCount);  
  29.     }  
  30.     return 0;  
  31. }  

运行结果如下图:

 

 

现在结果水落石出,明明有50个线程执行了g_nLoginCount++;操作,但结果输出是不确定的,有可能为50,但也有可能小于50

       要解决这个问题,我们就分析下g_nLoginCount++;操作。在VC6.0编译器对g_nLoginCount++;这一语句打个断点,再按F5进入调试状态,然后按下Debug工具栏的Disassembly按钮,这样就出现了汇编代码窗口。可以发现在C/C++语言中一条简单的自增语句其实是由三条汇编代码组成的,如下图所示。

 

 

讲解下这三条汇编意思:

第一条汇编将g_nLoginCount的值从内存中读取到寄存器eax中。

第二条汇编将寄存器eax中的值与1相加,计算结果仍存入寄存器eax中。

第三条汇编将寄存器eax中的值写回内存中。

       这样由于线程执行的并发性,很可能线程A执行到第二句时,线程B开始执行,线程B将原来的值又写入寄存器eax中,这样线程A所主要计算的值就被线程B修改了。这样执行下来,结果是不可预知的——可能会出现50,可能小于50。

       因此在多线程环境中对一个变量进行读写时,我们需要有一种方法能够保证对一个值的递增操作是原子操作——即不可打断性,一个线程在执行原子操作时,其它线程必须等待它完成之后才能开始执行该原子操作。这种涉及到硬件的操作会不会很复杂了,幸运的是,Windows系统为我们提供了一些以Interlocked开头的函数来完成这一任务(下文将这些函数称为Interlocked系列函数)。

下面列出一些常用的Interlocked系列函数:

1.增减操作

LONG__cdeclInterlockedIncrement(LONG volatile* Addend);

LONG__cdeclInterlockedDecrement(LONG volatile* Addend);

返回变量执行增减操作之后的值

LONG__cdec InterlockedExchangeAdd(LONG volatile* AddendLONGValue);

返回运算后的值,注意!加个负数就是减。

 

2.赋值操作

LONG__cdeclInterlockedExchange(LONG volatile* TargetLONGValue);

Value就是新值,函数会返回原先的值。

 

在本例中只要使用InterlockedIncrement()函数就可以了。将线程函数代码改成:

[cpp]  view plain  copy
 
  1. DWORD WINAPI ThreadFun(void *pPM)  
  2. {  
  3.     Sleep(100);//some work should to do  
  4.     //g_nLoginCount++;  
  5.     InterlockedIncrement((LPLONG)&g_nLoginCount);  
  6.     Sleep(50);  
  7.     return 0;  
  8. }  

再次运行,可以发现结果会是唯一的。

 

       因此,在多线程环境下,我们对变量的自增自减这些简单的语句也要慎重思考,防止多个线程导致的数据访问出错。更多介绍,请访问MSDN上Synchronization Functions这一章节,地址为 http://msdn.microsoft.com/zh-cn/library/aa909196.aspx

 

看到这里,相信本系列首篇《秒杀多线程第一篇 多线程笔试面试题汇总》中选择题第一题(百度笔试题)应该可以秒杀掉了吧(知其然也知其所以然),正确答案是D。另外给个附加问题,程序中是用50个线程模拟用户登录,有兴趣的同学可以试下用100个线程来模拟一下(上机试试绝对会有意外发现^_^)。

 

下一篇《秒杀多线程第四篇 一个经典多线程同步问题》将提出一个稍为复杂点但却非常经典的多线程同步互斥问题,这个问题会采用不同的方法来解答,从而让你充分熟练多线程同步互斥的“招式”。更多精彩,欢迎继续参阅。

转载第三部分

 

经典线程同步互斥问题

//经典线程同步互斥问题
#include <stdio.h>
#include <process.h>
#include <windows.h>

long g_nNum; //全局资源
unsigned int __stdcall Fun(void *pPM); //线程函数
const int THREAD_NUM = 10; //子线程个数

int main()
{
	g_nNum = 0;
	HANDLE  handle[THREAD_NUM];
	
	int i = 0;
	while (i < THREAD_NUM) 
	{
		handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
		i++;//等子线程接收到参数时主线程可能改变了这个i的值
	}
	//保证子线程已全部运行结束
	WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
	return 0;
}

unsigned int __stdcall Fun(void *pPM)
{
//由于创建线程是要一定的开销的,所以新线程并不能第一时间执行到这来
	int nThreadNum = *(int *)pPM; //子线程获取参数
	Sleep(50);//some work should to do
	g_nNum++;  //处理全局资源
	Sleep(0);//some work should to do
	printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
	return 0;
}

输出的结果比较混乱和随机性。牵扯到

一、主线程与子线程之间的同步以及

二、各子线程间的互斥

解决此问题的方式有以下几种:

1、关键段(临界区CS)

2、事件(Event

3、互斥量(Mutex

4、信号量

 

关键段

 

 

本文首先介绍下如何使用关键段,然后再深层次的分析下关键段的实现机制与原理。

关键段CRITICAL_SECTION一共就四个函数,使用很是方便。下面是这四个函数的原型和使用说明。

 

函数功能:初始化

函数原型:

void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);

函数说明:定义关键段变量后必须先初始化。

 

函数功能:销毁

函数原型:

void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);

函数说明:用完之后记得销毁。

 

函数功能:进入关键区域

函数原型:

void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);

函数说明:系统保证各线程互斥的进入关键区域。

 

函数功能:离开关关键区域

函数原型:

void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);

 

然后在经典多线程问题中设置二个关键区域。一个是主线程在递增子线程序号时,另一个是各子线程互斥的访问输出全局资源时。详见代码:

[cpp]  view plain  copy
 
  1. #include <stdio.h>  
  2. #include <process.h>  
  3. #include <windows.h>  
  4. long g_nNum;  
  5. unsigned int __stdcall Fun(void *pPM);  
  6. const int THREAD_NUM = 10;  
  7. //关键段变量声明  
  8. CRITICAL_SECTION  g_csThreadParameter, g_csThreadCode;  
  9. int main()  
  10. {  
  11.     printf("     经典线程同步 关键段\n");  
  12.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  13.   
  14.     //关键段初始化  
  15.     InitializeCriticalSection(&g_csThreadParameter);  
  16.     InitializeCriticalSection(&g_csThreadCode);  
  17.       
  18.     HANDLE  handle[THREAD_NUM];   
  19.     g_nNum = 0;   
  20.     int i = 0;  
  21.     while (i < THREAD_NUM)   
  22.     {  
  23.         EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域  
  24.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);  
  25.         ++i;  
  26.     }  
  27.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  28.   
  29.     DeleteCriticalSection(&g_csThreadCode);  
  30.     DeleteCriticalSection(&g_csThreadParameter);  
  31.     return 0;  
  32. }  
  33. unsigned int __stdcall Fun(void *pPM)  
  34. {  
  35.     int nThreadNum = *(int *)pPM;   
  36.     LeaveCriticalSection(&g_csThreadParameter);//离开子线程序号关键区域  
  37.   
  38.     Sleep(50);//some work should to do  
  39.   
  40.     EnterCriticalSection(&g_csThreadCode);//进入各子线程互斥区域  
  41.     g_nNum++;  
  42.     Sleep(0);//some work should to do  
  43.     printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);  
  44.     LeaveCriticalSection(&g_csThreadCode);//离开各子线程互斥区域  
  45.     return 0;  
  46. }  

运行结果如下图:

 

可以看出来,各子线程已经可以互斥的访问与输出全局资源了,但主线程与子线程之间的同步还是有点问题。

       这是为什么了?

要解开这个迷,最直接的方法就是先在程序中加上断点来查看程序的运行流程。断点处置示意如下:

 

然后按F5进行调试,正常来说这两个断点应该是依次轮流执行,但实际调试时却发现不是如此,主线程可以多次通过第一个断点即

       EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域

这一语句。这说明主线程能多次进入这个关键区域!找到主线程和子线程没能同步的原因后,下面就来分析下原因的原因吧^_^

 

先找到关键段CRITICAL_SECTION的定义吧,在WinBase.h中被定义成RTL_CRITICAL_SECTION。而RTL_CRITICAL_SECTION在WinNT.h中声明,它其实是个结构体

typedef struct _RTL_CRITICAL_SECTION {

    PRTL_CRITICAL_SECTION_DEBUGDebugInfo;

    LONGLockCount;

    LONGRecursionCount;

    HANDLEOwningThread; // from the thread's ClientId->UniqueThread

    HANDLELockSemaphore;

    DWORDSpinCount;

RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

各个参数的解释如下:

第一个参数:PRTL_CRITICAL_SECTION_DEBUGDebugInfo;

调试用的。

 

第二个参数:LONGLockCount;

初始化为-1n表示有n个线程在等待。

 

第三个参数:LONGRecursionCount;  

表示该关键段的拥有线程对此资源获得关键段次数,初为0

 

第四个参数:HANDLEOwningThread;  

即拥有该关键段的线程句柄,微软对其注释为——from the thread's ClientId->UniqueThread

 

第五个参数:HANDLELockSemaphore;

实际上是一个自复位事件。

 

第六个参数:DWORDSpinCount;    

旋转锁的设置,单CPU下忽略

 

由这个结构可以知道关键段会记录拥有该关键段的线程句柄即关键段是有“线程所有权”概念的。事实上它会用第四个参数OwningThread来记录获准进入关键区域的线程句柄,如果这个线程再次进入,EnterCriticalSection()会更新第三个参数RecursionCount以记录该线程进入的次数并立即返回让该线程进入。其它线程调用EnterCriticalSection()则会被切换到等待状态,一旦拥有线程所有权的线程调用LeaveCriticalSection()使其进入的次数为0时,系统会自动更新关键段并将等待中的线程换回可调度状态。

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

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

 

另外,由于将线程切换到等待状态的开销较大,因此为了提高关键段的性能,Microsoft将旋转锁合并到关键段中,这样EnterCriticalSection()会先用一个旋转锁不断循环,尝试一段时间才会将线程切换到等待状态。下面是配合了旋转锁的关键段初始化函数

函数功能:初始化关键段并设置旋转次数

函数原型:

BOOLInitializeCriticalSectionAndSpinCount(

  LPCRITICAL_SECTIONlpCriticalSection,

  DWORDdwSpinCount);

函数说明:旋转次数一般设置为4000。

 

函数功能:修改关键段的旋转次数

函数原型:

DWORDSetCriticalSectionSpinCount(

  LPCRITICAL_SECTIONlpCriticalSection,

  DWORDdwSpinCount);

 

Windows核心编程》第五版的第八章推荐在使用关键段的时候同时使用旋转锁,这样有助于提高性能。值得注意的是如果主机只有一个处理器,那么设置旋转锁是无效的。无法进入关键区域的线程总会被系统将其切换到等待状态。

 

 

最后总结下关键段:

1.关键段共初始化化、销毁、进入和离开关键区域四个函数。

2.关键段可以解决线程的互斥问题,但因为具有“线程所有权”,所以无法解决同步问题。

3.推荐关键段与旋转锁配合使用。

 

 

 

事件

上一篇中使用关键段来解决经典的多线程同步互斥问题,由于关键段的“线程所有权”特性所以关键段只能用于线程的互斥而不能用于同步。本篇介绍用事件Event来尝试解决这个线程同步问题。

首先介绍下如何使用事件。事件Event实际上是个内核对象,它的使用非常方便。下面列出一些常用的函数。

 

第一个 CreateEvent

函数功能:创建事件

函数原型:

HANDLECreateEvent(

 LPSECURITY_ATTRIBUTESlpEventAttributes,

 BOOLbManualReset,

 BOOLbInitialState,

 LPCTSTRlpName

);

函数说明:

第一个参数表示安全控制,一般直接传入NULL

第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所以有人都可以进入直到老师去关上教室门(事件变成未触发)。自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。

第三个参数表示事件的初始状态,传入TRUR表示已触发。

第四个参数表示事件的名称,传入NULL表示匿名事件。

 

第二个 OpenEvent

函数功能:根据名称获得一个事件句柄。

函数原型:

HANDLEOpenEvent(

 DWORDdwDesiredAccess,

 BOOLbInheritHandle,

 LPCTSTRlpName     //名称

);

函数说明:

第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示事件句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。

 

第三个SetEvent

函数功能:触发事件

函数原型:BOOLSetEvent(HANDLEhEvent);

函数说明:每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。

 

第四个ResetEvent

函数功能:将事件设为末触发

函数原型:BOOLResetEvent(HANDLEhEvent);

 

最后一个事件的清理与销毁

由于事件是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。

 

在经典多线程问题中设置一个事件和一个关键段。用事件处理主线程与子线程的同步,用关键段来处理各子线程间的互斥。详见代码:

[cpp]  view plain  copy
 
  1. #include <stdio.h>  
  2. #include <process.h>  
  3. #include <windows.h>  
  4. long g_nNum;  
  5. unsigned int __stdcall Fun(void *pPM);  
  6. const int THREAD_NUM = 10;  
  7. //事件与关键段  
  8. HANDLE  g_hThreadEvent;  
  9. CRITICAL_SECTION g_csThreadCode;  
  10. int main()  
  11. {  
  12.     printf("     经典线程同步 事件Event\n");  
  13.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  14.     //初始化事件和关键段 自动置位,初始无触发的匿名事件  
  15.     g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);   
  16.     InitializeCriticalSection(&g_csThreadCode);  
  17.   
  18.     HANDLE  handle[THREAD_NUM];   
  19.     g_nNum = 0;  
  20.     int i = 0;  
  21.     while (i < THREAD_NUM)   
  22.     {  
  23.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);  
  24.         WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发  
  25.         i++;  
  26.     }  
  27.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  28.   
  29.     //销毁事件和关键段  
  30.     CloseHandle(g_hThreadEvent);  
  31.     DeleteCriticalSection(&g_csThreadCode);  
  32.     return 0;  
  33. }  
  34. unsigned int __stdcall Fun(void *pPM)  
  35. {  
  36.     int nThreadNum = *(int *)pPM;   
  37.     SetEvent(g_hThreadEvent); //触发事件  
  38.       
  39.     Sleep(50);//some work should to do  
  40.       
  41.     EnterCriticalSection(&g_csThreadCode);  
  42.     g_nNum++;  
  43.     Sleep(0);//some work should to do  
  44.     printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);   
  45.     LeaveCriticalSection(&g_csThreadCode);  
  46.     return 0;  
  47. }  

运行结果如下图:

 

可以看出来,经典线线程同步问题已经圆满的解决了——线程编号的输出没有重复,说明主线程与子线程达到了同步。全局资源的输出是递增的,说明各子线程已经互斥的访问和输出该全局资源。

 

现在我们知道了如何使用事件,但学习就应该要深入的学习,何况微软给事件还提供了PulseEvent()函数,所以接下来再继续深挖下事件Event,看看它还有什么秘密没。

先来看看这个函数的原形:

第五个PulseEvent

函数功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。

函数原型:BOOLPulseEvent(HANDLEhEvent);

函数说明:这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种:

1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。

2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。

此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态

 

       下面对这个触发一个事件脉冲PulseEvent ()写一个例子,主线程启动7个子线程,其中有5个线程Sleep(10)后对一事件调用等待函数(称为快线程),另有2个线程Sleep(100)后也对该事件调用等待函数(称为慢线程)。主线程启动所有子线程后再Sleep(50)保证有5个快线程都正处于等待状态中。此时若主线程触发一个事件脉冲,那么对于手动置位事件,这5个线程都将顺利执行下去。对于自动置位事件,这5个线程中会有中一个顺利执行下去。而不论手动置位事件还是自动置位事件,那2个慢线程由于Sleep(100)所以会错过事件脉冲,因此慢线程都会进入等待状态而无法顺利执行下去。

代码如下:

[cpp]  view plain  copy
 
  1. //使用PluseEvent()函数  
  2. #include <stdio.h>  
  3. #include <conio.h>  
  4. #include <process.h>  
  5. #include <windows.h>  
  6. HANDLE  g_hThreadEvent;  
  7. //快线程  
  8. unsigned int __stdcall FastThreadFun(void *pPM)  
  9. {  
  10.     Sleep(10); //用这个来保证各线程调用等待函数的次序有一定的随机性  
  11.     printf("%s 启动\n", (PSTR)pPM);  
  12.     WaitForSingleObject(g_hThreadEvent, INFINITE);  
  13.     printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM);  
  14.     return 0;  
  15. }  
  16. //慢线程  
  17. unsigned int __stdcall SlowThreadFun(void *pPM)  
  18. {  
  19.     Sleep(100);  
  20.     printf("%s 启动\n", (PSTR)pPM);  
  21.     WaitForSingleObject(g_hThreadEvent, INFINITE);  
  22.     printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM);  
  23.     return 0;  
  24. }  
  25. int main()  
  26. {  
  27.     printf("  使用PluseEvent()函数\n");  
  28.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  29.   
  30.     BOOL bManualReset = FALSE;  
  31.     //创建事件 第二个参数手动置位TRUE,自动置位FALSE  
  32.     g_hThreadEvent = CreateEvent(NULL, bManualReset, FALSE, NULL);  
  33.     if (bManualReset == TRUE)  
  34.         printf("当前使用手动置位事件\n");  
  35.     else  
  36.         printf("当前使用自动置位事件\n");  
  37.   
  38.     char szFastThreadName[5][30] = {"快线程1000""快线程1001""快线程1002""快线程1003""快线程1004"};  
  39.     char szSlowThreadName[2][30] = {"慢线程196""慢线程197"};  
  40.   
  41.     int i;  
  42.     for (i = 0; i < 5; i++)  
  43.         _beginthreadex(NULL, 0, FastThreadFun, szFastThreadName[i], 0, NULL);  
  44.     for (i = 0; i < 2; i++)  
  45.         _beginthreadex(NULL, 0, SlowThreadFun, szSlowThreadName[i], 0, NULL);  
  46.       
  47.     Sleep(50); //保证快线程已经全部启动  
  48.     printf("现在主线程触发一个事件脉冲 - PulseEvent()\n");  
  49.     PulseEvent(g_hThreadEvent);//调用PulseEvent()就相当于同时调用下面二句  
  50.     //SetEvent(g_hThreadEvent);  
  51.     //ResetEvent(g_hThreadEvent);  
  52.       
  53.     Sleep(3000);   
  54.     printf("时间到,主线程结束运行\n");  
  55.     CloseHandle(g_hThreadEvent);  
  56.     return 0;  
  57. }  

自动置位事件,运行结果如下:

 

手动置位事件,运行结果如下:

 

 

最后总结下事件Event

1.事件是内核对象,事件分为手动置位事件自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。

2.事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。

3.事件可以解决线程间同步问题,因此也能解决互斥问题。

 

后面二篇《秒杀多线程第七篇 经典线程同步 互斥量Mutex》和《秒杀多线程第八篇 经典线程同步 信号量Semaphore》将介绍如何使用互斥量和信号量来解决这个经典线程同步问题。欢迎大家继续秒杀多线程之旅。

 

 

 

互斥量

 

前面介绍了关键段CS事件Event经典线程同步问题中的使用。本篇介绍用互斥量Mutex来解决这个问题。

互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问。互斥量与关键段的行为非常相似,并且互斥量可以用于不同进程中的线程互斥访问资源。使用互斥量Mutex主要将用到四个函数。下面是这些函数的原型和使用说明。

第一个 CreateMutex

函数功能:创建互斥量(注意与事件Event的创建函数对比)

函数原型:

HANDLECreateMutex(

  LPSECURITY_ATTRIBUTESlpMutexAttributes,

  BOOLbInitialOwner,     

  LPCTSTRlpName

);

函数说明:

第一个参数表示安全控制,一般直接传入NULL

第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。

第三个参数用来设置互斥量的名称,在多个进程中的线程就是通过名称来确保它们访问的是同一个互斥量。

函数访问值:

成功返回一个表示互斥量的句柄,失败返回NULL。

 

第二个打开互斥量

函数原型:

HANDLEOpenMutex(

 DWORDdwDesiredAccess,

 BOOLbInheritHandle,

 LPCTSTRlpName     //名称

);

函数说明:

第一个参数表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示互斥量句柄继承性,一般传入TRUE即可。

第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。

函数访问值:

成功返回一个表示互斥量的句柄,失败返回NULL。

 

第三个触发互斥量

函数原型:

BOOLReleaseMutex (HANDLEhMutex)

函数说明:

访问互斥资源前应该要调用等待函数,结束访问时就要调用ReleaseMutex()来表示自己已经结束访问,其它线程可以开始访问了。

 

最后一个清理互斥量

由于互斥量是内核对象,因此使用CloseHandle()就可以(这一点所有内核对象都一样)。

 

接下来我们就在经典多线程问题用互斥量来保证主线程与子线程之间的同步,由于互斥量的使用函数类似于事件Event,所以可以仿照上一篇的实现来写出代码

[cpp]  view plain  copy
 
  1. //经典线程同步问题 互斥量Mutex  
  2. #include <stdio.h>  
  3. #include <process.h>  
  4. #include <windows.h>  
  5.   
  6. long g_nNum;  
  7. unsigned int __stdcall Fun(void *pPM);  
  8. const int THREAD_NUM = 10;  
  9. //互斥量与关键段  
  10. HANDLE  g_hThreadParameter;  
  11. CRITICAL_SECTION g_csThreadCode;  
  12.   
  13. int main()  
  14. {  
  15.     printf("     经典线程同步 互斥量Mutex\n");  
  16.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  17.       
  18.     //初始化互斥量与关键段 第二个参数为TRUE表示互斥量为创建线程所有  
  19.     g_hThreadParameter = CreateMutex(NULL, FALSE, NULL);  
  20.     InitializeCriticalSection(&g_csThreadCode);  
  21.   
  22.     HANDLE  handle[THREAD_NUM];   
  23.     g_nNum = 0;   
  24.     int i = 0;  
  25.     while (i < THREAD_NUM)   
  26.     {  
  27.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);  
  28.         WaitForSingleObject(g_hThreadParameter, INFINITE); //等待互斥量被触发  
  29.         i++;  
  30.     }  
  31.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  32.       
  33.     //销毁互斥量和关键段  
  34.     CloseHandle(g_hThreadParameter);  
  35.     DeleteCriticalSection(&g_csThreadCode);  
  36.     for (i = 0; i < THREAD_NUM; i++)  
  37.         CloseHandle(handle[i]);  
  38.     return 0;  
  39. }  
  40. unsigned int __stdcall Fun(void *pPM)  
  41. {  
  42.     int nThreadNum = *(int *)pPM;  
  43.     ReleaseMutex(g_hThreadParameter);//触发互斥量  
  44.       
  45.     Sleep(50);//some work should to do  
  46.   
  47.     EnterCriticalSection(&g_csThreadCode);  
  48.     g_nNum++;  
  49.     Sleep(0);//some work should to do  
  50.     printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);  
  51.     LeaveCriticalSection(&g_csThreadCode);  
  52.     return 0;  
  53. }  

运行结果如下图:

 

可以看出,与关键段类似,互斥量也是不能解决线程间的同步问题。

       联想到关键段会记录线程ID即有“线程拥有权”的,而互斥量也记录线程ID,莫非它也有“线程拥有权”这一说法。

       答案确实如此,互斥量也是有“线程拥有权”概念的。“线程拥有权”在关键段中有详细的说明,这里就不再赘述了。另外由于互斥量常用于多进程之间的线程互斥,所以它比关键段还多一个很有用的特性——“遗弃”情况的处理。比如有一个占用互斥量的线程在调用ReleaseMutex()触发互斥量前就意外终止了(相当于该互斥量被“遗弃”了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被触发而陷入一个无穷的等待过程中了?这显然不合理。因为占用某个互斥量的线程既然终止了那足以证明它不再使用被该互斥量保护的资源,所以这些资源完全并且应当被其它线程来使用。因此在这种“遗弃”情况下,系统自动把该互斥量内部的线程ID设置为0,并将它的递归计数器复置为0,表示这个互斥量被触发了。然后系统将公平地选定一个等待线程来完成调度(被选中的线程的WaitForSingleObject()会返回WAIT_ABANDONED_0)。

 

下面写二个程序来验证下:

第一个程序创建互斥量并等待用户输入后就触发互斥量。第二个程序先打开互斥量,成功后就等待并根据等待结果作相应的输出。详见代码:

第一个程序:

[cpp]  view plain  copy
 
  1. #include <stdio.h>  
  2. #include <conio.h>  
  3. #include <windows.h>  
  4. const char MUTEX_NAME[] = "Mutex_MoreWindows";  
  5. int main()  
  6. {  
  7.     HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME); //创建互斥量  
  8.     printf("互斥量已经创建,现在按任意键触发互斥量\n");  
  9.     getch();  
  10.     //exit(0);  
  11.     ReleaseMutex(hMutex);  
  12.     printf("互斥量已经触发\n");  
  13.     CloseHandle(hMutex);  
  14.     return 0;  
  15. }  

第二个程序:

[cpp]  view plain  copy
 
  1. #include <stdio.h>  
  2. #include <windows.h>  
  3. const char MUTEX_NAME[] = "Mutex_MoreWindows";  
  4. int main()  
  5. {  
  6.     HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, MUTEX_NAME); //打开互斥量  
  7.     if (hMutex == NULL)  
  8.     {  
  9.         printf("打开互斥量失败\n");  
  10.         return 0;  
  11.     }  
  12.     printf("等待中....\n");  
  13.     DWORD dwResult = WaitForSingleObject(hMutex, 20 * 1000); //等待互斥量被触发  
  14.     switch (dwResult)  
  15.     {  
  16.     case WAIT_ABANDONED:  
  17.         printf("拥有互斥量的进程意外终止\n");  
  18.         break;  
  19.   
  20.     case WAIT_OBJECT_0:  
  21.         printf("已经收到信号\n");  
  22.         break;  
  23.   
  24.     case WAIT_TIMEOUT:  
  25.         printf("信号未在规定的时间内送到\n");  
  26.         break;  
  27.     }  
  28.     CloseHandle(hMutex);  
  29.     return 0;  
  30. }  

运用这二个程序时要先启动程序一再启动程序二。下面展示部分输出结果:

结果一.二个进程顺利执行完毕:

 

结果二.将程序一中//exit(0);前面的注释符号去掉,这样程序一在触发互斥量之前就会因为执行exit(0);语句而且退出,程序二会收到WAIT_ABANDONED消息并输出“拥有互斥量的进程意外终止”:

 

有这个对“遗弃”问题的处理,在多进程中的线程同步也可以放心的使用互斥量。

 

最后总结下互斥量Mutex

1.互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。

2.互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。

信号量

 

前面介绍了关键段CS事件Event互斥量Mutex在经典线程同步问题中的使用。本篇介绍用信号量Semaphore来解决这个问题。

首先也来看看如何使用信号量,信号量Semaphore常用有三个函数,使用很方便。下面是这几个函数的原型和使用说明。

第一个 CreateSemaphore

函数功能:创建信号量

函数原型:

HANDLE CreateSemaphore(

  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

  LONG lInitialCount,

  LONG lMaximumCount,

  LPCTSTR lpName

);

函数说明:

第一个参数表示安全控制,一般直接传入NULL

第二个参数表示初始资源数量。

第三个参数表示最大并发数量。

第四个参数表示信号量的名称,传入NULL表示匿名信号量。

 

第二个 OpenSemaphore

函数功能:打开信号量

函数原型:

HANDLE OpenSemaphore(

  DWORD dwDesiredAccess,

  BOOL bInheritHandle,

  LPCTSTR lpName

);

函数说明:

第一个参数表示访问权限,对一般传入SEMAPHORE_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示信号量句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个信号量。

 

第三个 ReleaseSemaphore

函数功能:递增信号量的当前资源计数

函数原型:

BOOL ReleaseSemaphore(

  HANDLE hSemaphore,

  LONG lReleaseCount,  

  LPLONG lpPreviousCount 

);

函数说明:

第一个参数是信号量的句柄。

第二个参数表示增加个数,必须大于0且不超过最大资源数量。

第三个参数可以用来传出先前的资源计数,设为NULL表示不需要传出。

 

注意:当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。 

 

最后一个 信号量的清理与销毁

由于信号量是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。

 

在经典多线程问题中设置一个信号量和一个关键段。用信号量处理主线程与子线程的同步,用关键段来处理各子线程间的互斥。详见代码:

[cpp]  view plain  copy
 
  1. #include <stdio.h>  
  2. #include <process.h>  
  3. #include <windows.h>  
  4. long g_nNum;  
  5. unsigned int __stdcall Fun(void *pPM);  
  6. const int THREAD_NUM = 10;  
  7. //信号量与关键段  
  8. HANDLE            g_hThreadParameter;  
  9. CRITICAL_SECTION  g_csThreadCode;  
  10. int main()  
  11. {  
  12.     printf("     经典线程同步 信号量Semaphore\n");  
  13.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  14.   
  15.     //初始化信号量和关键段  
  16.     g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);//当前0个资源,最大允许1个同时访问  
  17.     InitializeCriticalSection(&g_csThreadCode);  
  18.   
  19.     HANDLE  handle[THREAD_NUM];   
  20.     g_nNum = 0;  
  21.     int i = 0;  
  22.     while (i < THREAD_NUM)   
  23.     {  
  24.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);  
  25.         WaitForSingleObject(g_hThreadParameter, INFINITE);//等待信号量>0  
  26.         ++i;  
  27.     }  
  28.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  29.       
  30.     //销毁信号量和关键段  
  31.     DeleteCriticalSection(&g_csThreadCode);  
  32.     CloseHandle(g_hThreadParameter);  
  33.     for (i = 0; i < THREAD_NUM; i++)  
  34.         CloseHandle(handle[i]);  
  35.     return 0;  
  36. }  
  37. unsigned int __stdcall Fun(void *pPM)  
  38. {  
  39.     int nThreadNum = *(int *)pPM;  
  40.     ReleaseSemaphore(g_hThreadParameter, 1, NULL);//信号量++  
  41.   
  42.     Sleep(50);//some work should to do  
  43.   
  44.     EnterCriticalSection(&g_csThreadCode);  
  45.     ++g_nNum;  
  46.     Sleep(0);//some work should to do  
  47.     printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);  
  48.     LeaveCriticalSection(&g_csThreadCode);  
  49.     return 0;  
  50. }  

运行结果如下图:

 

可以看出来,信号量也可以解决线程之间的同步问题。

 

由于信号量可以计算资源当前剩余量并根据当前剩余量与零比较来决定信号量是处于触发状态或是未触发状态,因此信号量的应用范围相当广泛。本系列的《秒杀多线程第十篇 生产者消费者问题》将再次使用它来解决线程同步问题,欢迎大家参阅。

 

转载第六部分——总结

 

 

 

前面《秒杀多线程第四篇一个经典的多线程同步问题》提出了一个经典的多线程同步互斥问题,这个问题包括了主线程与子线程的同步,子线程间的互斥,是一道非常经典的多线程同步互斥问题范例,后面分别用了四篇

秒杀多线程第五篇经典线程同步关键段CS

秒杀多线程第六篇经典线程同步事件Event

秒杀多线程第七篇经典线程同步互斥量Mutex

秒杀多线程第八篇经典线程同步信号量Semaphore

来详细介绍常用的线程同步互斥机制——关键段、事件、互斥量、信号量。下面对它们作个总结,帮助大家梳理各个知识点。

 

首先来看下关于线程同步互斥的概念性的知识,相信大家通过前面的文章,已经对线程同步互斥有一定的认识了,也能模糊的说出线程同步互斥的各种概念性知识,下面再列出从《计算机操作系统》一书中选取的一些关于线程同步互斥的描述。相信先有个初步而模糊的印象再看下权威的定义,应该会记忆的特别深刻。

 

1.线程(进程)同步的主要任务

答:在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。

 

2.线程(进程)之间的制约关系?

当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。

(1).间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享CPU,共享I/O设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程A在使用打印机时,其它线程都要等待。

(2).直接相互制约。这种制约主要是因为线程之间的合作,如有线程A将计算结果提供给线程B作进一步处理,那么线程B在线程A将数据送达之前都将处于阻塞状态。

间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程A和线程B互斥访问某个资源则它们之间就会产个顺序问题——要么线程A等待线程B操作完毕,要么线程B等待线程操作完毕,这其实就是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步

 

3.临界资源和临界区

在一段时间内只允许一个线程访问的资源就称为临界资源或独占资源,计算机中大多数物理设备,进程中的共享变量等待都是临界资源,它们要求被互斥的访问。每个进程中访问临界资源的代码称为临界区

 

看完概念性知识,下面用几个表格来帮助大家更好的记忆和运用多线程同步互斥的四个实现方法——关键段、事件、互斥量、信号量。

 

关键段CS与互斥量Mutex

 

 

创建或初始化

销毁

进入互斥区域

离开互斥区域

关键段CS

Initialize-

CriticalSection

Delete-

CriticalSection

Enter-

CriticalSection

Leave-

CriticalSection

互斥量Mutex

CreateMutex

CloseHandle

等待系列函数如WaitForSingleObject

ReleaseMutex

 

 

关键段与互斥量都有“线程所有权”概念,可以将“线程所有权”理解成旅馆的房卡,在旅馆前台登记名字拥有房卡后是可以多次进出房间的,其它人则无法进入直到你交出房卡。每个线程必须先通过EnterCriticalSectionWaitForSingleObject来尝试获得“线程所有权”才能调用LeaveCriticalSectionReleaseMutex。否则会调用失败,这就相当于伪造房卡去办理退房手续——由于登记本上没有你的名字所以会被拒绝。

互斥量能很好的处理“遗弃”情况,因此在多进程之间可以放心的使用。

 

事件Event

 

 

创建

销毁

使事件触发

使事件未触发

事件Event

CreateEvent

CloseHandle

SetEvent

ResetEvent

 

 

注意事件的手动置位和自动置位要分清楚,不要混淆了。

 

信号量Semaphore

 

 

创建

销毁

递减计数

递增计数

信号量

Semaphore

Create-

Semaphore

CloseHandle

等待系列函数如WaitForSingleObject

Release-

Semaphore

 

 

信号量在计数大于0时表示触发状态,调用WaitForSingleObject不会阻塞,等于0表示未触发状态,调用WaitForSingleObject会阻塞直到有其它线程递增了计数。

 

注意:互斥量,事件,信号量都是内核对象,可以跨进程使用(通过OpenMutexOpenEventOpenSemaphore)。不过为什么只有互斥量能解决“遗弃”情况了,请看《秒杀多线程第十五篇 关键段,事件,互斥量,信号量的“遗弃”问题》。

 

呵呵^_^,本系列一共使用了六篇文章来讲解了上面三个表格,如果读者能轻松写出这个表格并能解释下各函数的用法,那么对多线程的同步互斥问题也就有了良好的基础。

 

通过经典线程同步问题的学习,我们已经初步练好了解决多线程同步互斥的各种“招式”,下面再通过学习二个著名的实例《秒杀多线程第十篇 生产者消费者问题》和《秒杀多线程第十一篇 读者写者问题》来使我们在解决多线程同步时更加熟练。

 

使用互斥锁可以实现线程同步

 

#include <iostream>
#include <windows.h>
using namespace std;

HANDLE hMutex = NULL;//互斥量
//线程函数
DWORD WINAPI Fun(LPVOID lpParamter)
{
    for (int i = 0; i < 10; i++)
    {
        //请求一个互斥量锁
        WaitForSingleObject(hMutex, INFINITE);
        cout << "A Thread Fun Display!" << endl;
        Sleep(100);
        //释放互斥量锁
        ReleaseMutex(hMutex);
    }
    return 0L;//表示返回的是long型的0

}

int main()
{
    //创建一个子线程
    HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
    hMutex = CreateMutex(NULL, FALSE,L"screen");
    //关闭线程
    CloseHandle(hThread);
    //主线程的执行路径
    for (int i = 0; i < 10; i++)
    {
        //请求获得一个互斥量锁
        WaitForSingleObject(hMutex,INFINITE);
        cout << "Main Thread Display!" << endl;
        Sleep(100);
        //释放互斥量锁
        ReleaseMutex(hMutex);
    }
    return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值