windows系统c++多线程开发

线程的一些基本概念

一、线程的基本概念。

基本概念:线程,即轻量级进程(LWP:LightWeight Process),是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程不拥有系统资源,近拥有少量运行必须的资源。

二、线程的基本状态。

基本状态:就绪、阻塞和运行三种基本状态。

就绪状态,指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;

运行状态,指线程占有处理机正在运行;

阻塞状态,指线程在等待一个事件(如信号量),逻辑上不可执行。

三、进程和线程的关系。

简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

四、线程同步互斥的4种方式

1.  临界区(Critical Section):适合一个进程内的多线程访问公共区域或代码段时使用

2.  互斥量 (Mutex):适合不同进程内多线程访问公共区域或代码段时使用,与临界区相似。

3.  事件(Event):通过线程间触发事件实现同步互斥

4.  信号量(Semaphore):与临界区和互斥量不同,可以实现多个线程同时访问公共区域数据,原理与操作系统PV操作类似,先设置一个访问公共区域的线程最大连接数,每有一个线程访问共享区资源数就减一,直到资源数小于等于零。

 

互斥:关键段CS与互斥量Mutex

 

创建或初始化

销毁

进入互斥区域

离开互斥区域

关键段CS

Initialize-

CriticalSection

Delete-

CriticalSection

Enter-

CriticalSection

Leave-

CriticalSection

互斥量Mutex

CreateMutex

CloseHandle

等待系列函数如WaitForSingleObject

ReleaseMutex

同步:

事件Event

 

创建

销毁

使事件触发

使事件未触发

事件Event

CreateEvent

CloseHandle

SetEvent

ResetEvent

多条线程之间的互斥:

信号量Semaphore

 

创建

销毁

递减计数

递增计数

信号量

Semaphore

Create-

Semaphore

CloseHandle

等待系列函数如WaitForSingleObject

Release-

Semaphore

实现同步、互斥的四个方法的详解:

1、《关键段CriticalSection》的例子

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

关键段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_SECTIONWinNT.h中声明,它其实是个结构体

typedefstruct _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;

初始化为-1,n表示有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.推荐关键段与旋转锁配合使用。

 

 2、《事件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.事件可以解决线程间同步问题,因此也能解决互斥问题。

3、《互斥量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.互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。

4、《信号量Semaphone

首先也来看看如何使用信号量,信号量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.}  

运行结果如下图:

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

 

由于信号量可以计算资源当前剩余量并根据当前剩余量与零比较来决定信号量是处于触发状态或是未触发状态,因此信号量的应用范围相当广泛。

 

五、多线程中栈与堆是公有的还是私有的。

在多线程环境下,每个线程拥有一个栈和一个程序计数器。栈和程序计数器用来保存线程的执行历史和线程的执行状态,是线程私有的资源。其他的资源(比如堆、地址空间、全局变量)是由同一个进程内的多个线程共享。

六、Windows编程中互斥量与临界区比较类似,请分析一下二者的主要区别。

临界区不能用于进程间同步, 它不属于内核对象, 所以速度比较快;

互斥量可以用于进程间同步, 属于内核对象, 速度较慢。

1、    临界区只能用于对象在同一进程里线程间的互斥访问;互斥体可以用于对象进程间或线程间的互斥访问;

2、    临界区是非内核对象,只在用户态进行锁操作,速度快;

3、    互斥体是内核对象,在核心态进行锁操作,速度慢。

4、    临界区和互斥体在Windows平台都下可用;Linux下只有互斥体可用。

七、多线程中多参数传递。

 我们先来看一个简单的程序:

[cpp] view plain copy

 

1. #include <stdio.h>  

2. #include <windows.h>  

3.   

4. DWORD WINAPI ThreadFun(LPVOID pM)  

5. {  

6.     printf("%s\n", pM);  

7.     return 0;  

8. }  

9.   

10.int main()  

11.{  

12.    printf("main thread\n");  

13.    HANDLE handle = CreateThread(NULL, 0, ThreadFun, "hello world", 0, NULL);  

14.    CloseHandle(handle);  

15.  

16.    Sleep(2000);  

17.  

18.    return 0;  

19.}  

      在主线程中,传递给线程函数ThreadFun的串为"helloworld".CreateThread函数的限制,只能传递LPVOID,糟糕了,如果你要传递很多东东给子线程,那么,该怎么办呢?请看下面的程序:

[cpp] view plain copy

 

1. #include <stdio.h>  

2. #include <windows.h>  

3.   

4. typedef struct test  

5. {  

6.     int n;  

7.     char c;  

8.     char a[100];  

9. }Test;  

10.  

11.DWORD WINAPI ThreadFun(LPVOID pM)  

12.{  

13.    Test *pt = (Test *)pM;  

14.    printf("%d, %c, %s\n", pt->n, pt->c, pt->a);  

15.    return 0;  

16.}  

17.  

18.int main()  

19.{  

20.    printf("main thread\n");  

21.  

22.    Test t;  

23.    t.n = 1;  

24.    t.c = 'x';  

25.    memset(t.a, 0, sizeof(t.a));  

26.    strncpy(t.a, "so great"sizeof(t.a) - 1);  

27.    HANDLE handle = CreateThread(NULL, 0, ThreadFun, (LPVOID)&t, 0, NULL);  

28.    CloseHandle(handle);  

29.  

30.    Sleep(2000);  

31.  

32.    return 0;  

33.}  

      这种方式,无处不在,无处不见。下次见了,要认识哈。

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/462145.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

centos 下安装配置nfs服务器

1、环境centos5.6_x64iptables 关闭selinux 开启&#xff08;这个好像不影响&#xff09;服务端&#xff1a;192.168.10.150客户端&#xff1a;192.168.10.1522、安装yum install portmap //默认已安装yum install nfs-utils3、创建共享目录mkdir /nfsvim /etc/exports/nfs 19…

使用FTP获取RFC文档

连接FTP服务器主机&#xff1a; ftp FTP.RFC-EDITOR.ORG 21&#xff08;21是FTP中控制连接的端口号&#xff09;输入用户名&#xff1a;anonymous输入密码&#xff1a;namehost.domainRFC文档是放在in-notes/rfcxxxx.txt中的&#xff08;xxxx是RFC文档的编号&#xff09;&#…

XP系统安装SQL2000企业版开启1433端口

1.获得SQL2000安装包2.获得SQL2000.MSDE-KB884525-SP4-CHS.EXE/SQL2000-KB884525-SP4-X86-CHS.EXE补丁包微软官网有下载3.在SQL2000 的安装盘中找到MSDE这个目录&#xff0c;并且点击setup.exe安装&#xff0c;完成后重启。4.再次安装SQL2000客户端工具&#xff0c;完成后重启5…

GPU编程语言选择(OpenCL、CUDA 与C++ AMP)

1、CUDA、OpenCL与C AMP 其实在C AMP之前已经有了两个异构编程框架&#xff1a;CUDA与OpenCL。CUDA&#xff08;Compute Unified Device Architecture&#xff09;是显卡厂商Nvidia于2007年推出的业界第一款异构并行编程框架。在Nvidia的大力支持下&#xff0c;CUDA拥有良好的开…

初见 IsolatedStorage

IsolatedStorage 翻译过来是(隔离存储空间) 怎么理解呢,jake lin的故事讲得很好,可以拿过来借用一下 " 朝鲜人民精神文明都非常的发达, 因此上网时都不需要访问internet. 因此不可能访问我们的网络.也不能访问其他精神文明没有他们发达的国家的网络. 我们也不能访问朝鲜的…

单链表的头插法和尾插法实现代码(无头结点)

/*头指针,可以发现head是赋值为NULL&#xff0c;而不是head->next*/ //-----------------头插法------------------- node *head, *p; head NULL; while (……) {p (node *)malloc(……);p->data ……;p->next head;//此时第一个p的next指针是NULL&#xff0c;其后…

iptables的SNAT和DNAT应用

首先开启路由转发功能&#xff1a;# vim /etc/sysctl.confnet.ipv4.ip_forward 1# sysctl -p&#xff08;1&#xff09;SNAT&#xff1a;内网主机 --访问--> 外网服务器<1> 搭建模拟环境内网PC1&#xff1a;172.16.0.1/16&#xff08;Host-Only&#xff09;网关PC2&a…

[Winodows Phone 7控件详解]容器控件

在Windows Phone7中存在着多个容器控件&#xff0c;这些控件主要是用来界面的布局设置&#xff0c;以及包容多个控件时的布局设置。 一.Grid控件&#xff1a;主要用于界面的布局&#xff0c;这个和web page里的很相似&#xff0c;可以通过网格布置规划界面&#xff0c;也可以嵌…

C++ Template

引言 模板&#xff08;Template&#xff09;指C程序设计设计语言中采用类型作为参数的程序设计&#xff0c;支持通用程序设计。C 的标准库提供许多有用的函数大多结合了模板的观念&#xff0c;如STL以及IO Stream。 函数模板 在c入门中&#xff0c;很多人会接触swap(int&, …

《数据结构与算法分析》学习笔记(二)——算法分析

一、对算法分析方法的最简单的理解和使用方法 1、首先大家可能一般会被那些数学的概念搞晕&#xff0c;其实简单理解下来&#xff0c;就是假设任何语句执行的效率都是一样的&#xff0c;所以设定每一个语句的执行时间都是一个时间单位&#xff0c;那么只要计算这个程序到底执行…

Oracle--plsql异常处理

•什么是异常?Oracle中出现错误的情形通常分为编译时错误&#xff08;compile-timeerror&#xff09;和运行时错误(run-time error)&#xff0c;异常是在PL/SQL执行过程中出现的警告或错误。•异常是如何触发的?–发生了一个 Oracle 错误时–使用RAISE语句显式触发•如何处理…

再谈SQL Server中日志的的作用

简介 之前我已经写了一个关于SQL Server日志的简单系列文章。本篇文章会进一步挖掘日志背后的一些概念&#xff0c;原理以及作用。如果您没有看过我之前的文章&#xff0c;请参阅&#xff1a; 浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架 浅谈SQL Server中的事…

树的遍历和图的遍历的异同

一、认识的理清 1、 应该认识到“深度优先”和“广度优先”是算法思想&#xff0c;而递归是实现“深度优先”的一种方法&#xff08;深度优先可以非递归实现&#xff09;&#xff1b; 2、 深度优先搜索也叫深度优先遍历&#xff08;DFS&#xff0c;Depth_Fisrt_Search&#x…

VS2012和XE2013的关联和设置问题

1. 首先要正确安装Microsoft VisualStudio 2012 Intel Parallel Studio XE 2013&#xff0c;其中&#xff0c;先安装VS2012&#xff0c;再安装XE2013。 2.Microsoft Visual Studio 2012 Intel Parallel Studio XE 2013集成检验&#xff1a; 打开&#xff1a;开始所有程序-&…

语文成绩

题目背景 语文考试结束了&#xff0c;成绩还是一如既往地有问题。 题目描述 语文老师总是写错成绩&#xff0c;所以当她修改成绩的时候&#xff0c;总是累得不行。她总是要一遍遍地给某些同学增加分数&#xff0c;又要注意最低分是多少。你能帮帮她吗&#xff1f; //这又跟神器…

最小生成树和最短路径

一、概念的理清 1、图分为连通图和非连通图&#xff0c;我们一般讨论连通图。带权的图叫做网。 2、连通图的生成树&#xff1a;&#xff08;1&#xff09;包含图的所有的n个顶点&#xff1b;&#xff08;2&#xff09;只有n-1条边&#xff0c;且这n-1条边足以构成一棵树&…

推荐一款移动端的web UI控件 -- mobiscroll

用mobiscroll 可实现ios系统自带的选择器控件效果&#xff0c;支持几乎所有的移动平台(iOS, Android, BlackBerry, Windows Phone 8, Amazon Kindle)&#xff0c;当然在pc的浏览器上跑&#xff0c;效果也还不错。建议使用支持css3的浏览器访问^_^ 支持换肤&#xff0c;效果和性…

I2C通信 读写数据过程

在通信之初&#xff0c;主从机必须根据自己的要求约定好通信规则&#xff1a;command的定义和位置、address的位数和位置。 以读写从机寄存器数据为例&#xff1a; 假设从机寄存器地址为8位、从机寄存器也位8位&#xff08;被读取数据为8位&#xff09;&#xff1b; 约定读comm…

C++的运算符重载(转)

C的运算符重载 cc扩展语言 C中预定义的运算符的操作对象只能是基本数据类型。但实际上&#xff0c;对于许多用户自定义类型&#xff08;例如类&#xff09;&#xff0c;也需要类似的运算操作。这时就必须在C中重新定义这些运算符&#xff0c;赋予已有运算符新的功能&#xff0c…

Android 用户界面---样式和主题(Styles and Themes)(二)

样式属性 理解了样式是如何定义的之后&#xff0c;就需要学习<item>元素都定义了那些有效的样式属性类型。你可能已经熟悉了像layout_width和textColor属性&#xff0c;但是还有更多的可以使用的样式属性。 查找应用于指定的View对象的最好的地方是对应的类参考&#xff…