针对一个经典的线程同步互斥问题,前面几篇文章提出了四种解决方案:关键段、事件、互斥量、信号量。
下面对这四种解决方案做一个总结,梳理一下知识点:
首先来看下关于线程同步互斥的概念性的知识,相信大家通过前面的文章,已经对线程同步互斥有一定的认识了,也能模糊的说出线程同步互斥的各种概念性知识,下面再列出从《计算机操作系统》一书中选取的一些关于线程同步互斥的描述。相信先有个初步而模糊的印象再看下权威的定义,应该会记忆的特别深刻。
1.线程(进程)同步的主要任务
答:在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量、表格、链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行结果具有可再现性。
2.线程(进程)之间的制约关系?
当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。
(1)间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享CPU、共享I/O设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程A在使用打印机时,其它线程都要等待。
(2)直接相互制约。这种制约主要是因为线程之间的合作,如有线程A将计算结果提供给线程B作进一步处理,那么线程B在线程A将数据送达之前都将处于阻塞状态。
间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程A和线程B互斥访问某个资源则它们之间就会产个顺序问题——要么线程A等待线程B操作完毕,要么线程B等待线程操作完毕,这其实就是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步。
3.临界资源和临界区
在一段时间内只允许一个线程访问的资源就称为临界资源或独占资源,计算机中大多数物理设备,进程中的共享变量等待都是临界资源,它们要求被互斥的访问。每个进程中访问临界资源的代码称为临界区。
看完概念性知识,下面用几个表格来帮助大家更好的记忆和运用多线程同步互斥的四个实现方法——关键段、事件、互斥量、信号量。
关键段CS与互斥量Mutex:
注意:关键段比互斥量要快,同时如果关键段结合旋转锁时,速度更快。当时互斥量是内核对象,能够跨进程互斥,同时能很好的解决“遗弃”的问题。所以在同一个进程中的互斥的时候,尽量用关键段;不同进程互斥的时候,只能选择互斥量,不能选择关键段。
互斥量由于是内核对象,同时又有线程所有权的概念,所以可以被用来限定程序的启动次数。防止一个程序启动多个相同的进程,但是同一个进程可以多次跑,因为Mutex记录了该线程ID。
事件Event:
注意事件的手动置位和自动置位要分清楚,不要混淆了。
注意:事件一般是用来同步的,很少用来做互斥,应为它没有关键段CS和互斥量Mutex的优势。Event处于无信号状态时,相关线程或进程退出,系统并不会尝试将其置为有信号状态,也就是说不能解决“遗弃”的问题。
信号量Semaphore:
信号量在计数大于0时表示触发状态,调用WaitForSingleObject不会阻塞,等于0表示未触发状态,调用WaitForSingleObject会阻塞直到有其它线程递增了计数。
注意:互斥量,事件,信号量都是内核对象,可以跨进程使用(通过OpenMutex,OpenEvent,OpenSemaphore)。
线程同步中的遗弃问题
互斥量常用于多进程之间的线程互斥,所以它比关键段还多一个很有用的特性——“遗弃”情况的处理。比如有一个占用互斥量的线程在调用ReleaseMutex()触发互斥量前就意外终止了(相当于该互斥量被“遗弃”了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被触发而陷入一个无穷的等待过程中了?这显然不合理。因为占用某个互斥量的线程既然终止了那足以证明它不再使用被该互斥量保护的资源,所以这些资源完全并且应当被其它线程来使用。因此在这种“遗弃”情况下,系统自动把该互斥量内部的线程ID设置为0,并将它的递归计数器复置为0,表示这个互斥量被触发了。然后系统将“公平地”选定一个等待线程来完成调度(被选中的线程的WaitForSingleObject()会返回WAIT_ABANDONED_0)。
可见“遗弃”问题就是——占有某种资源的进程意外终止后,其它等待该资源的进程能否感知。
1. 关键段的“遗弃”问题
关键段在这个问题上很简单——由于关键段不能跨进程使用,所以关键段不需要处理“遗弃”问题。
注:这段话没怎么理解,暂且认为关键段不能夸进程,同一个进程里的线程不会随便的死掉。
2. 事件,互斥量,信号量的“遗弃”问题
事件、互斥量、信号量都是内核对象,可以跨进程使用。一个进程在创建一个命名的事件后,其它进程可以调用OpenEvent()并传入事件的名称来获得这个事件的句柄。因此事件,互斥量和信号量都会遇到“遗弃”问题。我们已经知道互斥量能够处理“遗弃”问题,接下来就来看看事件和信号量是否能够处理“遗弃”问题。
下面做一组具体的实验:
1. 创建两个进程。
2. 进程一创建一个初始为未触发的事件,然后等待按键,按下y则触发事件后结束进程,否则直接退出表示进程一已意外终止。
3. 进程二先获得事件的句柄,然后调用WaitForSingleObject()等待这个事件10秒,在这10秒内如果事件已经触发则输出“已收到信号”,否则输出“未在规定的时间内收到信号”。如果在等待的过程中进程一意外终止,则输出“拥有事件的进程意外终止”。信号量的试验方法类似。
互斥量的前面已经测试过了,下面测试事件有关“遗弃”问题的实验:
进程一:
#include <stdio.h> #include <windows.h> const TCHAR STR_EVENT_NAME[] = TEXT("Event_Name"); int main() { //建立事件,自动置位,当前未触发 HANDLE hEvent = CreateEvent(NULL, false, false, STR_EVENT_NAME); printf("事件已经创建,现在按y触发事件,按其它终止进程 "); char ch; scanf("%c", &ch); if (ch != 'y') { exit(0); } SetEvent(hEvent); printf("事件已经被触发 "); CloseHandle(hEvent); return 0; }
进程二:
#include <stdio.h> #include <process.h> #include <windows.h> const TCHAR STR_EVENT_NAME[] = TEXT("Event_Name"); int main() { HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS, true, STR_EVENT_NAME); if (hEvent == NULL) { printf("打开事件失败 "); return 0; } printf("等待中... "); DWORD dwResult = WaitForSingleObject(hEvent, 20*1000); switch (dwResult) { case WAIT_ABANDONED: printf("拥有事件的线程意外终止 "); break; case WAIT_OBJECT_0: printf("已经收到信号 "); break; case WAIT_TIMEOUT: printf("未在规定的时间内收到信号 "); break; } CloseHandle(hEvent); return 0; }
测试的结果:
可以看出进程二没能感知进程一意外终止,说明事件不能处理“遗弃”问题
下面测试信号量的“遗弃”问题的出题:
进程一:
#include <stdio.h> #include <windows.h> const TCHAR STR_SEMAPHORE_NAME[] = TEXT("Event_Name"); int main() { //建立事件,自动置位,当前未触发 HANDLE hSemaphore = CreateSemaphore(NULL, 0, 1, STR_SEMAPHORE_NAME); printf("事件已经创建,现在按y触发事件,按其它终止进程 "); char ch; scanf("%c", &ch); if (ch != 'y') { exit(0); } ReleaseSemaphore(hSemaphore, 1, 0); printf("事件已经被触发 "); CloseHandle(hSemaphore); return 0; }
进程二:
#include <stdio.h> #include <process.h> #include <windows.h> const TCHAR STR_SEMAPHORE_NAME[] = TEXT("Event_Name"); int main() { HANDLE hSemaphore = OpenSemaphore(SEMAPHORE_ALL_ACCESS, true, STR_SEMAPHORE_NAME); if (hSemaphore == NULL) { printf("打开信号量失败 "); return 0; } printf("等待中... "); DWORD dwResult = WaitForSingleObject(hSemaphore, 20*1000); switch (dwResult) { case WAIT_ABANDONED: printf("拥有事件的线程意外终止 "); break; case WAIT_OBJECT_0: printf("已经收到信号 "); break; case WAIT_TIMEOUT: printf("未在规定的时间内收到信号 "); break; } CloseHandle(hSemaphore); return 0; }
可以看出进程二没能感知进程一意外终止,说明信号量与事件一样都不能处理“遗弃”问题。
3. 遗弃问题的总结
由本文所做的试验可知,互斥量能够处理“遗弃”情况,事件与信号量都无法解决这一情况。
再思考下互斥量能处理“遗弃”问题的原因,其实正是因为它有“线程所有权”概念。在系统中一旦有线程结束后,系统会判断是否有互斥量被这个线程占有,如果有,系统会将这互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这表示该互斥量已经不为任何线程占用,处于触发状态。其它等待这个互斥量的线程就能顺利执行下去了。
1. 线程所有权绑定了执行线程,所以互斥量的WaitForSingleObject和ReleaseEvent、关键段的EnterCriticalSection和LeaveCriticalSection都必须在同一个线程中执行。这样绑定了执行线程,使得同一个线程可以多次进入变量访问区,所以不能实现线程的同步,但是可用于线程的互斥,某一时间段只允许同一个线程访问。
2. 不能跨进程,是因为关键段不是内核对象,没有Open函数打开别人定义的内核对象。