线程的一些基本概念
一、线程的基本概念。
基本概念:线程,即轻量级进程(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
#include <stdio.h> #include <process.h> #include <windows.h> long g_nNum; unsigned int __stdcall Fun(void *pPM); const int THREAD_NUM = 10; //关键段变量声明 CRITICAL_SECTION g_csThreadParameter, g_csThreadCode; int main() { printf(" 经典线程同步 关键段\n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); //关键段初始化 InitializeCriticalSection(&g_csThreadParameter); InitializeCriticalSection(&g_csThreadCode); HANDLE handle[THREAD_NUM]; g_nNum = 0; int i = 0; while (i < THREAD_NUM) { EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域 handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL); ++i; } WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); DeleteCriticalSection(&g_csThreadCode); DeleteCriticalSection(&g_csThreadParameter); return 0;
} unsigned int __stdcall Fun(void *pPM) { int nThreadNum = *(int *)pPM; LeaveCriticalSection(&g_csThreadParameter);//离开子线程序号关键区域 Sleep(50);//some work should to do EnterCriticalSection(&g_csThreadCode);//进入各子线程互斥区域 g_nNum++; Sleep(0);//some work should to do printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum); LeaveCriticalSection(&g_csThreadCode);//离开各子线程互斥区域 return 0;
}
运行结果如下图:
可以看出来,各子线程已经可以互斥的访问与输出全局资源了,但主线程与子线程之间的同步还是有点问题。
这是为什么了?
要解开这个迷,最直接的方法就是先在程序中加上断点来查看程序的运行流程。断点处置示意如下:
然后按F5进行调试,正常来说这两个断点应该是依次轮流执行,但实际调试时却发现不是如此,主线程可以多次通过第一个断点即
EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域
这一语句。这说明主线程能多次进入这个关键区域!找到主线程和子线程没能同步的原因后,下面就来分析下原因的原因吧^_^
先找到关键段CRITICAL_SECTION的定义吧,它在WinBase.h中被定义成RTL_CRITICAL_SECTION。而RTL_CRITICAL_SECTION在WinNT.h中声明,它其实是个结构体:
typedefstruct _RTL_CRITICAL_SECTION {PRTL_CRITICAL_SECTION_DEBUGDebugInfo;LONGLockCount;LONGRecursionCount;HANDLEOwningThread; // from the thread's ClientId->UniqueThreadHANDLELockSemaphore;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
#include <stdio.h> #include <process.h> #include <windows.h> long g_nNum; unsigned int __stdcall Fun(void *pPM); const int THREAD_NUM = 10; //事件与关键段 HANDLE g_hThreadEvent; CRITICAL_SECTION g_csThreadCode; int main()
{ printf(" 经典线程同步 事件Event\n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); //初始化事件和关键段 自动置位,初始无触发的匿名事件 g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL); InitializeCriticalSection(&g_csThreadCode); HANDLE handle[THREAD_NUM]; g_nNum = 0; int i = 0; while (i < THREAD_NUM) { handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL); WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发 i++; } WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); //销毁事件和关键段 CloseHandle(g_hThreadEvent); DeleteCriticalSection(&g_csThreadCode); return 0;
} unsigned int __stdcall Fun(void *pPM)
{ int nThreadNum = *(int *)pPM; SetEvent(g_hThreadEvent); //触发事件 Sleep(50);//some work should to do EnterCriticalSection(&g_csThreadCode); g_nNum++; Sleep(0);//some work should to do printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum); LeaveCriticalSection(&g_csThreadCode); return 0;
}
运行结果如下图:
可以看出来,经典线线程同步问题已经圆满的解决了——线程编号的输出没有重复,说明主线程与子线程达到了同步。全局资源的输出是递增的,说明各子线程已经互斥的访问和输出该全局资源。
现在我们知道了如何使用事件,但学习就应该要深入的学习,何况微软给事件还提供了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
//使用PluseEvent()函数 #include <stdio.h> #include <conio.h> #include <process.h> #include <windows.h> HANDLE g_hThreadEvent; //快线程 unsigned int __stdcall FastThreadFun(void *pPM)
{ Sleep(10); //用这个来保证各线程调用等待函数的次序有一定的随机性 printf("%s 启动\n", (PSTR)pPM); WaitForSingleObject(g_hThreadEvent, INFINITE); printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM); return 0;
} //慢线程 unsigned int __stdcall SlowThreadFun(void *pPM) { Sleep(100); printf("%s 启动\n", (PSTR)pPM); WaitForSingleObject(g_hThreadEvent, INFINITE); printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM); return 0;
} int main()
{ printf(" 使用PluseEvent()函数\n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); BOOL bManualReset = FALSE; //创建事件 第二个参数手动置位TRUE,自动置位FALSE g_hThreadEvent = CreateEvent(NULL, bManualReset, FALSE, NULL); if (bManualReset == TRUE) printf("当前使用手动置位事件\n"); else printf("当前使用自动置位事件\n"); char szFastThreadName[5][30] = {"快线程1000", "快线程1001", "快线程1002", "快线程1003", "快线程1004"}; char szSlowThreadName[2][30] = {"慢线程196", "慢线程197"}; int i; for (i = 0; i < 5; i++) _beginthreadex(NULL, 0, FastThreadFun, szFastThreadName[i], 0, NULL); for (i = 0; i < 2; i++) _beginthreadex(NULL, 0, SlowThreadFun, szSlowThreadName[i], 0, NULL); Sleep(50); //保证快线程已经全部启动 printf("现在主线程触发一个事件脉冲 - PulseEvent()\n"); PulseEvent(g_hThreadEvent);//调用PulseEvent()就相当于同时调用下面二句 //SetEvent(g_hThreadEvent); //ResetEvent(g_hThreadEvent); Sleep(3000); printf("时间到,主线程结束运行\n"); CloseHandle(g_hThreadEvent); return 0;
}
对自动置位事件,运行结果如下:
对手动置位事件,运行结果如下:
最后总结下事件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
//经典线程同步问题 互斥量Mutex #include <stdio.h> #include <process.h> #include <windows.h> long g_nNum; unsigned int __stdcall Fun(void *pPM); const int THREAD_NUM = 10; //互斥量与关键段 HANDLE g_hThreadParameter; CRITICAL_SECTION g_csThreadCode; int main()
{ printf(" 经典线程同步 互斥量Mutex\n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); //初始化互斥量与关键段 第二个参数为TRUE表示互斥量为创建线程所有 g_hThreadParameter = CreateMutex(NULL, FALSE, NULL); InitializeCriticalSection(&g_csThreadCode); HANDLE handle[THREAD_NUM]; g_nNum = 0; int i = 0; while (i < THREAD_NUM) { handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL); WaitForSingleObject(g_hThreadParameter, INFINITE); //等待互斥量被触发 i++; } WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); //销毁互斥量和关键段 CloseHandle(g_hThreadParameter); DeleteCriticalSection(&g_csThreadCode); for (i = 0; i < THREAD_NUM; i++) CloseHandle(handle[i]); return 0;
} unsigned int __stdcall Fun(void *pPM)
{ int nThreadNum = *(int *)pPM; ReleaseMutex(g_hThreadParameter);//触发互斥量 Sleep(50);//some work should to do EnterCriticalSection(&g_csThreadCode); g_nNum++; Sleep(0);//some work should to do printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum); LeaveCriticalSection(&g_csThreadCode); return 0;
}
运行结果如下图:
可以看出,与关键段类似,互斥量也是不能解决线程间的同步问题。
联想到关键段会记录线程ID即有“线程拥有权”的,而互斥量也记录线程ID,莫非它也有“线程拥有权”这一说法。
答案确实如此,互斥量也是有“线程拥有权”概念的。“线程拥有权”在关键段中有详细的说明,这里就不再赘述了。另外由于互斥量常用于多进程之间的线程互斥,所以它比关键段还多一个很有用的特性——“遗弃”情况的处理。比如有一个占用互斥量的线程在调用ReleaseMutex()触发互斥量前就意外终止了(相当于该互斥量被“遗弃”了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被触发而陷入一个无穷的等待过程中了?这显然不合理。因为占用某个互斥量的线程既然终止了那足以证明它不再使用被该互斥量保护的资源,所以这些资源完全并且应当被其它线程来使用。因此在这种“遗弃”情况下,系统自动把该互斥量内部的线程ID设置为0,并将它的递归计数器复置为0,表示这个互斥量被触发了。然后系统将“公平地”选定一个等待线程来完成调度(被选中的线程的WaitForSingleObject()会返回WAIT_ABANDONED_0)。
下面写二个程序来验证下:
第一个程序创建互斥量并等待用户输入后就触发互斥量。第二个程序先打开互斥量,成功后就等待并根据等待结果作相应的输出。详见代码:
第一个程序:
[cpp] view plain copy
#include <stdio.h> #include <conio.h> #include <windows.h> const char MUTEX_NAME[] = "Mutex_MoreWindows"; int main() { HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME); //创建互斥量 printf("互斥量已经创建,现在按任意键触发互斥量\n"); getch(); //exit(0); ReleaseMutex(hMutex); printf("互斥量已经触发\n"); CloseHandle(hMutex); return 0; } 个程序:p] view plain copy#include <stdio.h> #include <windows.h> const char MUTEX_NAME[] = "Mutex_MoreWindows"; int main()
{ HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, MUTEX_NAME); //打开互斥量 if (hMutex == NULL) { printf("打开互斥量失败\n"); return 0; } printf("等待中....\n"); DWORD dwResult = WaitForSingleObject(hMutex, 20 * 1000); //等待互斥量被触发 switch (dwResult) { case WAIT_ABANDONED: printf("拥有互斥量的进程意外终止\n"); break; case WAIT_OBJECT_0: printf("已经收到信号\n"); break; case WAIT_TIMEOUT: printf("信号未在规定的时间内送到\n"); break; } CloseHandle(hMutex); return 0;
}
运用这二个程序时要先启动程序一再启动程序二。下面展示部分输出结果:
结果一.二个进程顺利执行完毕:
结果二.将程序一中//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
#include <stdio.h> #include <process.h> #include <windows.h> long g_nNum; unsigned int __stdcall Fun(void *pPM); const int THREAD_NUM = 10; //信号量与关键段 HANDLE g_hThreadParameter; CRITICAL_SECTION g_csThreadCode; int main()
{ printf(" 经典线程同步 信号量Semaphore\n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); //初始化信号量和关键段 g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);//当前0个资源,最大允许1个同时访问 InitializeCriticalSection(&g_csThreadCode); HANDLE handle[THREAD_NUM]; g_nNum = 0; int i = 0; while (i < THREAD_NUM) { handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL); WaitForSingleObject(g_hThreadParameter, INFINITE);//等待信号量>0 ++i; } WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); //销毁信号量和关键段 DeleteCriticalSection(&g_csThreadCode); CloseHandle(g_hThreadParameter); for (i = 0; i < THREAD_NUM; i++) CloseHandle(handle[i]); return 0;
} unsigned int __stdcall Fun(void *pPM)
{ int nThreadNum = *(int *)pPM; ReleaseSemaphore(g_hThreadParameter, 1, NULL);//信号量++ Sleep(50);//some work should to do EnterCriticalSection(&g_csThreadCode); ++g_nNum; Sleep(0);//some work should to do printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum); LeaveCriticalSection(&g_csThreadCode); return 0;
}
运行结果如下图:
可以看出来,信号量也可以解决线程之间的同步问题。
由于信号量可以计算资源当前剩余量并根据当前剩余量与零比较来决定信号量是处于触发状态或是未触发状态,因此信号量的应用范围相当广泛。
五、多线程中栈与堆是公有的还是私有的。
在多线程环境下,每个线程拥有一个栈和一个程序计数器。栈和程序计数器用来保存线程的执行历史和线程的执行状态,是线程私有的资源。其他的资源(比如堆、地址空间、全局变量)是由同一个进程内的多个线程共享。
六、在Windows编程中互斥量与临界区比较类似,请分析一下二者的主要区别。
临界区不能用于进程间同步, 它不属于内核对象, 所以速度比较快;
互斥量可以用于进程间同步, 属于内核对象, 速度较慢。
1、 临界区只能用于对象在同一进程里线程间的互斥访问;互斥体可以用于对象进程间或线程间的互斥访问;
2、 临界区是非内核对象,只在用户态进行锁操作,速度快;
3、 互斥体是内核对象,在核心态进行锁操作,速度慢。
4、 临界区和互斥体在Windows平台都下可用;Linux下只有互斥体可用。
七、多线程中多参数传递。
我们先来看一个简单的程序:
[cpp] view plain copy
#include <stdio.h> #include <windows.h> DWORD WINAPI ThreadFun(LPVOID pM)
{ printf("%s\n", pM); return 0;
} int main()
{ printf("main thread\n"); HANDLE handle = CreateThread(NULL, 0, ThreadFun, "hello world", 0, NULL); CloseHandle(handle); Sleep(2000); return 0;
}
在主线程中,传递给线程函数ThreadFun的串为"helloworld".受CreateThread函数的限制,只能传递LPVOID,糟糕了,如果你要传递很多东东给子线程,那么,该怎么办呢?请看下面的程序:
[cpp] view plain copy
#include <stdio.h> #include <windows.h> typedef struct test
{ int n; char c; char a[100];
}Test; DWORD WINAPI ThreadFun(LPVOID pM)
{ Test *pt = (Test *)pM; printf("%d, %c, %s\n", pt->n, pt->c, pt->a); return 0;
} int main()
{ printf("main thread\n"); Test t; t.n = 1; t.c = 'x'; memset(t.a, 0, sizeof(t.a)); strncpy(t.a, "so great", sizeof(t.a) - 1); HANDLE handle = CreateThread(NULL, 0, ThreadFun, (LPVOID)&t, 0, NULL); CloseHandle(handle); Sleep(2000); return 0;
}
这种方式,无处不在,无处不见。下次见了,要认识哈。