zoukankan      html  css  js  c++  java
  • 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又有什么关系呢?我会在下一篇予以详细描述。

  • 相关阅读:
    如何打印调试字符串?
    如何测试代码执行耗时?
    access 如何导出 cvs 文件?
    opencv如何打印长图?
    window 注册表上下文菜单如何配置?
    python 零散记录(四) 强调字典中的键值唯一性 字典的一些常用方法
    python 零散记录(三) 格式化字符串 字符串相关方法
    python 零散记录(二) 序列的相关操作 相加 相乘 改变 复制
    python 零散记录(一) input与raw_input 数学相关函数 转换字符串的方法
    devi into python 笔记(七)locals与globals 字典格式化字符串 字符集
  • 原文地址:https://www.cnblogs.com/tobealion/p/4555481.html
Copyright © 2011-2022 走看看