进程与线程的互斥与同步:临界区、互斥量、信号量、事件、条件变量

一、为什么要进程与线程的互斥与同步——临界资源
    在多进程或多线程环境中,多个线程可能需要访问(读或写)同样的资源,而这些资源同一个时刻只能有一个线程访问,称为临界资源。临界资源可以是一个指向堆中某个对象的引用、一个打开的文件句柄、一个打开的网络socket或者是一个外部设备等等。多个线程对临界资源的同时访问可能造成意想不到的结果,例如,当一个线程正在修改一个变量时,另一个线程同时读取该变量,读取到的数据可能是脏数据。因此,需要线程的互斥访问临界资源。
    有时候一个线程必须等待另一个线程运行完获得结果后才能开始运行,这就需要线程之间的同步。
    互斥是同步的特殊情况,同步是更为复杂的互斥,同步了必然是互斥的。
二、临界区Critical Section
    每个线程中访问临界资源的那段程序称为临界区(Critical Section)(临界资源是一次仅允许一个线程使用的共享资源)。每次只允许一个线程进入临界区,进入后不允许其他线程进入。不论是硬件临界资源,还是软件临界资源,多个线程必须互斥地对它进行访问。
    在使用临界区时,一般不允许其运行时间过长,只要进入临界区的线程还没有离开,其他所有试图进入此临界区的线程都会被挂起而进入到等待状态,并会在一定程度上影响程序的运行性能。尤其需要注意的是不要将等待用户输入或是其他一些外界干预的操作包含到临界区。
    临界区不是内核对象,只能用于同一个进程的不同线程之间的同步。正因为临界区不是内核对象,相比于其它线程同步对象(互斥量、信号量、事件)速度要快一些。
三、信号量Semaphore
    信号量允许多个线程同时使用共享资源。信号量包含一个非负整数计数器,计数器的值表示同时访问共享资源的最大线程数(或者说可用资源的数目)。通过PV操作来获取、释放信号量。
    当一个线程进入临界区时,信号量计数减1(P操作),当该线程离开临界区时计数加1(V操作),P和V操作必须都是原子操作。当计数器的值为0时,其变为无信号状态,如果新来的线程要进入临界区(执行P操作),其必须阻塞等待计数器值增加,即有其它线程释放共享资源。
    若信号量初始计数器值为1,称为二进制信号量(Binary Semaphore)。
  【扩展阅读:1965年,荷兰计算机科学家Dijkstra引入二进制信号量,用以解决并发程序中的资源竞争问题。他的思想是使用在操作系统中使用一对方法来表示进入和离开临界区,这是通过获取和释放一种操作系统资源来实现的,这种资源便是信号量。Dijkstra称这对方法为P&V,P代表荷兰语Prolagen(实际是proberen te verlagen的组合词),意思是 尝试减小,V代表Verhogen,意思是增加。】
    信号量包括未命名(unnamed)信号量和命名(named)信号量,未命名信号是否能被多个进程使用取决于信号量的分配和初始化的方式。如果pshared的值为零,则不能在进程之间共享未命名信号量,只能在进程内部的线程之间共享;如果pshared的值不为零,则可以在进程之间共享信号量。
    使用信号量的内在风险:
     l  意外释放(Accidental release):某个线程可能没有获取信号量,但是执行了释放信号量的操作。
     l  递归死锁:一个线程尝试获取一个已经被它自己锁住的信号量。
     l  线程死亡造成死锁:获取到信号量的线程死亡或终止,未释放信号量,导致其它线程一直等待。这可以在获取信号量的操作时加上超时设置解决。
     l  循环死锁:多个线程循环等待释放信号量,如A->B->C->A,即A阻塞等待B释放某信号量,B阻塞等待C释放某信号量,C阻塞等待A释放某信号量。
       l  优先级倒置(反转):高优先级的线程被低优先级的线程一直阻塞。例如,三个线程A、B、C,A为高优先级,B为低优先级,C为中优先级。A和B通过信号量来控制对共享资源的访问,C不使用该共享资源。CPU调度C,A等待B释放信号量,由于C优先级高于B,CPU一直不调度B,导致具有高优先级的A也一直等待。
     l  信号量作为信号用于单向同步:信号量计数器初始值为0,线程1执行P操作阻塞等待信号量变为有信号状态,线程2执行V操作使得信号量变为有信号状态,实现线程2向线程1的单向同步。但是这种方法容易引起意外释放风险。
四、互斥量Mutex
    互斥量Mutex,即Mutual Exclusion。互斥量在没有任何线程占有时是有信号状态(signaled state),在被某个线程占有时是无信号状态(non-signaled state)。在同一个时刻,最多只能有一个线程占有互斥量,只有占有互斥量的对象才可以访问共享资源。当前占据资源的线程在任务处理完后应将拥有的互斥量对象交出(如Win32中的ReleaseMutex),以便其他线程在获得后得以访问资源。
    互斥量与二进制信号量的主要区别在于对象是否被占有。二进制信号量不会被线程占有,因此存在如第三节所述的内在风险。互斥量因为其“被线程占有”的属性,使得其可以解决二进制信号量的一些内在风险:
     l  意外释放:只有占有互斥量的线程才可以释放,其它线程释放将发生错误。
     l  递归死锁:占有互斥量的线程可以再次占有该互斥量,只要其释放次数与占有次数相同即可。
     l  优先级倒置(反转):处理优先级反转的方法有两种:优先级继承(priority inheritance)和优先级置顶(priority ceiling)策略。优先级继承指的是当出现高优先级的线程等待低优先级线程释放互斥量时,将低优先级的线程优先级暂时提高到高优先级,待线程运行完成后释放互斥量,再将线程调回原来的优先级。优先级置顶策略指的是每一个互斥量都被赋予一个优先级,这个优先级是所有申请或占有该互斥量的线程中的最高优先级,而对于占有该互斥量的线程,其优先级立马被提升为互斥量的优先级,即最高优先级。
     l  线程死亡造成死锁:线程死亡后操作系统可以检测该线程是否占有互斥量,如果占有可以通知等待该互斥量的线程。
     但相比于信号量,互斥量同样不能解决下面两个问题
     l  循环死锁:关于循环死锁发生的情形,举例如下:
 
  

线程一:
mutex_lock (ADC);
mutex_lock (DAC);
/* critical section */
mutex_unlock (ADC);
mutex_unlock (DAC);
线程二:
mutex_lock (DAC);
mutex_lock (ADC);
/* critical section */
mutex_unlock (DAC);
mutex_unlock (ADC);

    假设线程二优先级高于线程一,线程一获得ADC锁后。由于线程调度线程二运行,线程二获得DAC锁,线程二试图获得ADC锁,但由于ADC锁已被线程一获得,线程二阻塞等待。当线程一运行时,试图获得已被线程二获得的DAC锁,同样进入阻塞等待状态。如此便进入死锁状态。
    【扩展:死锁产生的4个必要条件是(1)mutual exclusion(资源互斥使用)、(2)hold and wait(持有某个资源并请求另一个资源)、(3)circular waiting(循环等待)、(4)No resource preemption(线程占有的资源在其使用完之前不会被抢占)。破坏这些条件便可以防止死锁,例如规定某个线程如果要获取多个互斥量,则要么全部获得,要么都不获得,破坏了持有某个资源并请求另一个资源的条件。又例如,前面提到的“优先级天花板策略” 也可以防止死锁,在上面的例子中,资源ADC和DAC的优先级都被赋予线程二的优先级,当线程一获得ADC锁后,线程二在尝试获得DAC锁时,系统检测到线程二优先级并不比已被线程一锁定的ADC优先级高,因此,系统阻塞线程二,使得线程一继续运行,阻止了死锁的产生。
     l  不合作(Non-cooperation):上述关于互斥量的使用依赖于一个基础的原则,即我们必须依赖于所有的任务(线程)都通过互斥量原语来访问,而实际上这依赖于软件的设计,操作系统无法检测到,这个问题由Tony Hoare解决,称为监视器Monitor。
    监视器是一种机制,一般不是RTOS提供的,而是程序员自己构建的。监视器通常只是简单地将共享资源和锁机制封装到一个结构中(比如封装了互斥机制的C++对象)。对于共享资源的访问,是通过不能被绕过的受控接口进行的。(即应用层并不显式调用互斥量,而是通过函数去间接调用)。
    互斥量是一种内核对象,可以用于不同进程的线程之间的同步(通过创建互斥量时的名称)。也正因为是内核对象,相关函数(如Win32环境下的ReleaseMutex、WaitForSingleObject等)执行时需要由用户态切换到内核态,耗时较长。
    如果在同一个进程中创建两个名称相同的互斥量,第二次创建时会失败;如果在不同进程中创建名称相同的互斥量,则第二个创建的进程将获得第一个进程创建的互斥量对象;如果创建互斥量时指定的名称和其它某对象名称相同,且其它某对象非互斥量对象,则创建失败。
五、事件Event
    事件是用于设置有信号状态(signaled state)和无信号状态(non-signaled state)的线程同步对象,它是通过通知操作的方式来保持线程的同步。线程只有等待事件变为有信号状态时才能继续执行,否则只能阻塞等待,例如Win32中的WaitForSingleObject与WaitForMultipleObjects即可用于等待事件有信号状态,WaitForSingleObject用于等待单个对象的完成,WaitForMultipleObjects用于等待多个对象的完成。事件有两种:手工复位事件(manual reset event,以下简称手工事件)和自动复位事件(auto reset event,以下简称自动事件)。这里的“复位”指的是将事件对象重置为无信号状态,使得其它线程不能访问共享资源,相应的,手工复位指的是需要手工调用复位函数(例如Win32中的ResetEvent(Handle event)函数)将事件重置为无信号状态来阻塞线程,而自动复位指的是当事件唤醒一个线程后系统立即自动将事件复位为无信号状态,不管等待事件的线程有多少个。因此,手工复位事件一次可以唤醒多个等待线程,而自动事件一次只能唤醒一个。
    事件是一种内核对象,可以用于不同进程的线程之间的同步。
六、条件变量(Condition Variable)
    条件变量是与互斥量相关联的一种用于多线程之间关于共享数据状态改变的通信机制,通俗地讲,是在多线程程序中用来实现“等待->唤醒”逻辑常用的方法。例如,应用程序A中包含两个线程t1和t2。t1需要在bool变量test_cond为true时才能继续执行,而test_cond的值是由t2来改变的,这种情况下,如何来写程序呢?可供选择的方案有两种:
    第一种是t1定时的去轮询变量test_cond,如果test_cond为false,则继续休眠;如果test_cond为true,则开始执行。
    第二种就是条件变量,t1在test_cond为false时调用cond_wait进行等待,t2在改变test_cond的值后,调用cond_signal,唤醒在等待中的t1,告诉t1 test_cond的值变了,这样t1便可继续往下执行。
    很明显,上面两种方案中,第二种方案是比较优的。在第一种方案中,在每次轮询时,如果t1休眠的时间比较短,会导致cpu浪费很厉害;如果t1休眠的时间比较长,又会导致应用逻辑处理不够及时,致使应用程序性能下降。第二种方案就是为了解决轮询的弊端而生的。对第二种方案程序示例如下:
 
  
pthread_mutex_t mutex;  /// 互斥锁
pthread_cond_t cond; /// 条件变量
bool test_cond = false;
/// TODO 初始化mutex和cond
 
/// thread 1:
pthread_mutex_lock
(&mutex);
while (!test_cond)
{
pthread_cond_wait
(&cond, &mutex);
}
pthread_mutex_unlock
(&mutex);
RunThread1Func();
 
/// thread 2:
pthread_mutex_lock
(&mutex);
test_cond
= true;
pthread_cond_signal
(&cond);
pthread_mutex_unlock
(&mutex);
 
/// TODO 销毁mutex和cond
    条件变量将解锁和挂起封装成为原子操作。等待一个条件变量时,会解开与该条件变量相关的锁,因此,使用条件变量等待的前提之一就是保证互斥量加锁。线程醒来之后,该互斥量会被自动加锁,所以,在完成相关操作之后需要解锁。例如,上面的示例中thread1和thread2的执行时序应该为:thread 1 lock->thread 1 wait-> thread 1 unlock(in wait)->thread 2 lock->thread 2 signal->thread 2 unlock->thread 1 lock(in wait)->thread 1 unlock。
    互斥量和条件变量的对应关系为1:N,即一个互斥量可以对应多个条件变量,一个条件变量只能对应一个互斥量。
七、常见问题
     1. 互斥量、自动复位事件、 二进制信号量(Binary Semaphore) 的区别
    互斥量和自动复位事件都可以使得在同一个时刻仅有一个线程能访问共享资源,对于互斥量,如果某个线程占用后其它线程只能等待直到占用的线程主动释放互斥量,而自动复位事件在通知某个线程有信号状态后,事件自动变为无信号状态,使得其它线程继续等待事件通知。两者看起来功能十分相似?
    互斥量可以看成是一个排他的令牌(token),同一个时刻仅允许有一个线程 占有。在某个线程占有互斥量期间,它“知道”其它线程都无法占用,自己在访问完共享资源后必须释放互斥量以便其它线程使用。而自动复位事件 不会被任何线程占有,自动复位事件就像一扇门,这扇门一次只允许一个线程通过,通过后这扇门就关闭(复位为无信号状态),关键是这扇门的打开(事件变为有信号状态) 不一定是由进入这扇门的线程控制的。
   互斥量一般用于线程之间排他性地访问共享资源,注重于“阻止”其他线程访问共享资源,其获取和释放必须由同一个线程完成,互斥量不能用于线程之间的同步。事件的使用注重于“通知”功能,用于通知等待该事件发生的线程可以进行自己的操作了,一般用于具有先后顺序的线程,即可用于线程之间的同步。
   
参考:
线程同步:https://www.codeproject.com/Articles/7953/Thread-Synchronization-for-Beginners
临界区:https://en.wikipedia.org/wiki/Critical_section
优先级反转:http://www.embedded.com/electronics-blogs/beginner-s-corner/4023947/Introduction-to-Priority-Inversion
https://www.quora.com/What-is-the-difference-between-critical-section-mutex-event-and-semaphore
手动复位与自动复位事件的区别:http://stackoverflow.com/questions/153877/what-is-the-difference-between-manualresetevent-and-autoresetevent-in-net
信号量与互斥量的区别:https://barrgroup.com/Embedded-Systems/How-To/RTOS-Mutex-Semaphore
使用信号量同步:http://docs.oracle.com/cd/E19120-01/open.solaris/816-5137/sync-11157/index.html
http://www.smxrtos.com/articles/techppr/mutex.htm
互斥量和信号量区别:https://blog.feabhas.com/2009/09/mutex-vs-semaphores-%E2%80%93-part-1-semaphores/
使用条件变量:http://docs.oracle.com/cd/E19120-01/open.solaris/816-5137/sync-21067/index.html
条件变量:http://blog.csdn.net/erickhuang1989/article/details/8754357
条件变量:http://hipercomer.blog.51cto.com/4415661/914841
http://blog.chinaunix.net/uid-23769728-id-3171090.html
http://blog.csdn.net/wind19/article/details/7520860
http://www.2cto.com/kf/201007/52946.html
http://blog.jobbole.com/86709/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值