Mutex, Semaphore and Monitor (1)

    虽然此前在多个项目中做过多线程方面的工作,但是对于Mutex(互斥锁)、Semaphore(信号量)以及Monitor(监视器/管程)的理解不是很透彻。在看了一些资料,做了些验证之后,总算有了些初步的认识。把这些认识放在这里,供大家参考,有错误还请指正!

    首先谈一下Mutex,这应该是在初学多线程开发时碰到最多也是最基本的同步方式。在Java中,当我们想对多线程共享的数据进行读写操作的时候,为了线程安全,我们一般都会在涉及该共享数据的读写方法上加上synchronized关键字,或者使用直接Lock对象。这样,只有首先进入synchronized或者Lock保护的代码块的线程,也就是获得了该对象锁的线程能够执行,其余试图执行该代码块的线程都会进入阻塞状态(JVM可能会有做一些SpinLock的优化,这里不考虑),直到获得锁的线程离开该代码块,释放锁。这就是MutexJava中的体现形式。而在C语言标准中,对于多线程编程来说,并没有提供原生的Mutex机制,但可以使用pthread库来完成,原理上来说应该是一样的。

    再来说说Semaphore。在大学的基础操作系统课中,讲到进程同步,Semaphore是无法绕过的一节。在我用的教材里,进程同步的三个经典问题:生产者与消费者问题、哲学家进餐问题、读者与写者问题,都是用Semaphore解决的,反倒Monitor是一页带过,跟我实际的项目经验恰恰相反。那么什么是Semaphore呢?其实Semaphore就是一个共享的计数器,在这个计数器上定义了两种操作:

  1. P操作。它是一个原子操作,等待直到这个计数器的值大于0,将其减1
  2. V操作。它同样是一个原子操作,它直接将计数器的值加1

从它的定义可以看出,如果我们用计数器来表示可用资源的数目,那么它很适合对一组资源进行同步控制,比如经典的生产者消费者问题。而且,如果Semaphore如果是一个二元Semaphore(通常你可以设置计数器初值为1),那么它就是一个Mutex

    下面我们以生产者消费者问题为例来理解下Semaphore的原理和使用方式。先描述下生产者消费者问题:生产者往缓冲区放一个物品,如果满了就等待;消费者从同一缓冲区取一个物品,如果空了就等待。 

Semaphore empty = N;

Semaphore full = 0;

Semaphore mutex = 1;

Producer() {

 

  int item;

  while (1) {

    item = Bake();

    P(empty);

    P(mutex);

    Insert(item);

    V(mutex);

    V(full);

   }

}

Consumer() {

 

  int item; 

  while (1) {

    P(full);

    P(mutex);

    item = Delete();

    V(mutex);

    V(empty);

    Eat(item);

  }

}

    在这个问题中我们使用了3个信号量。首先要理解,缓冲区的空和满都是共享资源,因而设置了emptyfull这两个信号量。empty初始值为Nfull初始值为0。生产者每次会把empty1,对应的把full1;而消费者刚好相反。比较易忽略的是mutex信号量,它是一个2元信号量,其实也就是一个mutex锁,它的作用是保证缓冲区的读写进(线)程安全。这是什么意思呢?比如说有多个Producer,如果没有P(mutex)的保护,它们可能会同时执行Insert(item)。这会造成一个问题,比如缓冲区是一个数组的话,那么Insert的实现方式可能就是,找到一个合适的位置i,执行buffer[i] = item,如果两个Producer找到的是同一个i,那么有一个ProducerItem就被覆盖了,所以需要通过mutex保证缓冲区的读写安全性。

    由此看来,信号量似乎很好地解决了同步问题了。但是,虽然对于消费者/生产者这种简单的模型,信号量似乎已经足够方便了,但是当问题变得稍微复杂一点,你会发现信号量就比较麻烦了。记得,我在学操作系统课,使用信号量解决读者/写者问题时,就已经有点糊涂了。你会发现,对于一个看似简单的问题,会搞出许多信号量来,而且其PV操作并不是成对的。信号量好像一个只能容纳有限人数的黑屋子,放了指定人数的人进去后,就关上门,什么也不管了。事情还是能做,但就是麻烦,且容易出错。说到底,还是因为信号量对于线程间的通知机制的描述不够。

  

    不过还好有Monitor。在谈Monitor之前,需要先了解下条件变量(CVCondition Variable),因为Monitor是基于CV的。先来介绍CV的定义:

一个条件变量是指这样一种条件:一个线程能够在改条件上等待,直到满足该条件;当条件满足时能够唤醒等待在该条件上的其他线程。

在条件变量上定义了3种操作:

  1. Wait() -- 阻塞调用线程直到有其他线程调用Signal或者Broadcast去唤醒它;
  2. Signal() -- 唤醒在该CV上等待的一个线程;
  3. Broadcast() -- 唤醒在该CV上等待的所有线程;

如果要在C中使用条件变量,可以使用pthread中的类型pthread_cond_t,其接口如下所示:

pthread_cond_init()  -- 初始化一个条件变量;

pthread_cond_wait(&theCV, &someMutex)   -- init产生的CV上等待;

pthread_cond_signal(&theCV)   -- 唤醒在CV上等待的某个线程;

pthread_cond_broadcast(&theCV) --唤醒在CV上等待的所有线程;

可以看到,使用CV时,需要与一个mutex配合使用,而且每个CV的操作,都需要在获得该mutex互斥锁的情况下进行。我们来看下条件变量的一个应用例子:

 

    这个代码的意思是,A一直处于等待状态,直到BCounter增为10时,唤醒A。如果不使用mutex(图中的myLock),对于A来说,可能会出现B已经将counter增为10,而A仍然认为counter小于10的情况,进而进入等待状态,背离了代码的本意。而且,会使得线程B的自增操作不是原子操作,导致出现一系列的问题,因此这个mutex是必须的。当然,这只是一个例子,不过,从更广泛的范围看,这个mutex都是不可或缺的。这也是pthread的接口,在调用wait时有个mutex参数的原因。

    我们再来看看,在进行CV操作的时候,其内部都发生了哪些事情,我们还是以上面那段代码为例,通过描述其具体过程,来了解CV操作。

   假设A执行pthread_mutex_lock,拿到了锁,B进入阻塞状态。然后由于此时counter<10成立,A执行pthread_cond_wait,这会导致两个结果:一是A会在CV上等待,二是A会释放锁。

   B由阻塞变为可执行状态,执行pthread_mutex_lock获得锁,将counter自增为1,由于counter >=10不成立,因此执行pthread_mutex_unlock释放锁。

   B循环执行,重新获得锁,自增,释放锁,直到counter == 10,执行pthread_cond_signal     唤醒线程A。这里需要注意的是,signal并不会释放锁,而 wait需要重新拿回锁才能返回,因而只有当B执行pthread_mutex_unlock后,A才有可能(因为B在循环执行,他可能会继续拿到锁)拿到锁,恢复执行。

所以CV的要点有三个:

  1. 所有CV操作需要在拿到互斥锁的情况下执行;
  2. 执行wait会等待并释放锁,直到有其他线程唤醒它,且它能重新拿到锁,才能恢复执行;
  3. 执行signal或者broadcast会唤醒线程,但并不会释放锁。

    那么CVMonitor又有什么关系呢?我会在下一篇予以详细描述。

 

posted @ 2015-06-05 19:51  码的一手好代码  阅读(2195)  评论(1编辑  收藏  举报