zoukankan      html  css  js  c++  java
  • 【C++工程实践】条件变量

    1、linux条件变量简介

    先看看linux下条件变量的api:

    1 #include <pthread.h> 
    2 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    3 int pthread_cond_signal(pthread_cond_t *cond);

    pthread_cond_wait:These functions atomically release mutex and cause the calling thread to block on the condition variable cond.

    pthread_cond_signal : call unblocks at least one of the threads that are blocked on the specified condition variable cond (if any threads are blocked on cond).

    这里包括了一个解锁的操作,会引入一个疑问,为什么wait里需要互斥器mutex?

    2、linux锁的使用

    对于自旋锁,相当于一直尝试获取锁:

    while (lock(mutex) == false) {
    }
    // do something

    如果其他thread一直持有该锁,会导致本线程一直while抢锁,浪费CPU做无用功.

    理论上不用一直判断lock,只需要在lock失败后判断mutex是否有变化即可。抢锁失败后只要锁的持有状态一直没有改变,那就让出 CPU 给别的线程先执行好了。

    这就是互斥锁:

    while (lock(mutex) == false) {
      thread sleep untile lock state change  
    }

    操作系统负责线程调度,为了实现锁的状态发生改变时再唤醒,mutex_lock sleep需要操作系统处理,因此pthread_mutex_lock涉及上下文切换,开销比较大。

    自旋锁和互斥锁都是保证能够排它地访问被锁保护的资源。

    3、条件变量分析

    很多情况下,我们并不需要完全排他性的占有某些资源,以生产者消费者为例:

    生产者向Queue中添加元素,消费者从Queue中消费元素,使用互斥锁mutex用于生产者/消费者Queue同步:

    lock(mutex); // mutex 保护对 queue 的操作
    while (queue.isEmpty()) { // 队列为空时等待
        unlock(mutex);
        // wait, 这里让出锁,让生产者有机会往 queue 里安放数据
        lock(mutex);
    }
    data = queue.pop(); // 至此肯定非空,所以能对资源进行操作
    unlock(mutex);
    consume(data); // 在临界区外做其它处理

    这里的while,相当于又搞出了一个自旋锁,一直等待queue非空。

    有了前面自旋锁、互斥器的经验就不难想到:「只要条件没有发生改变,while 里就没有必要再去解锁、 判断、条件不成立、加锁,完全可以让出 CPU 给别的线程」。不过由于「条件是否达成」属于业务逻辑, 操作系统没法管理,需要让能够作出这一改变的代码来手动「通知」,比如上面的例子里就需要在生产者 往 queue 里 push 后「通知」!queue.isEmpty() 成立。

    因此我们希望把while改成这种形式:

    while (queue.isEmpty()) {
        解锁后等待通知唤醒再加锁(用来收发通知的东西, lock);
    }

    而通知机制则为:

    触发通知(用来收发通知的东西);
    // 一般有两种方式:
    // 通知所有在等待的(notifyAll / broadcast)
    // 通知一个在等待的(notifyOne / signal)

    这就是条件变量,它解决的不是互斥,而是等待。

    上述的解锁后等待通知再加锁,就是

    pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

    换句话说,pthread_cond_wait本质上包含了三个操作:

        pthread_mutex_unlock(mtx);
        pthread_cond_just_wait(cv);
        pthread_mutex_lock(mtx);

    上面三行代码的并不是pthread_cond_wait(cv, mtx)的内联展开。其中第一行和第二行必须“原子化”,而第三行是可以分离出去的。那么为什么第一行和第二行不能分离呢?这是因为必须得保证:

    如果线程A先进入wait函数(即使没有进入实际的等待状态,比如正在释放mtx),那么必须得保证其他线程在其之后调用的broadcast必须能够将线程A唤醒。

    4、条件变量使用总结

    条件变量只有一种正确使用的方式,几乎不可能用错。对于 wait 端:
    1. 必须与 mutex 一起使用,该布尔表达式的读写需受此 mutex 保护。
    2. 在 mutex 已上锁的时候才能调用 wait()。
    3. 把判断布尔条件和 wait() 放到 while 循环中。

    对于 signal/broadcast 端:
    1. 不一定要在 mutex 已上锁的情况下调用 signal (理论上)。
    2. 在 signal 之前一般要修改布尔表达式。
    3. 修改布尔表达式通常要用 mutex 保护(至少用作 full memory barrier)。
    4. 注意区分 signal 与 broadcast:“broadcast 通常用于表明状态变化,signal 通常用于表示资源可用。(broadcast should generally be used to indicate state change rather than resource availability。)”

    如果用条件变量来实现一个“事件等待器/Waiter”,正确的做法是怎样的?我的最终答案见 WaiterInMuduo class。“事件等待器”的一种用途是程序启动时等待初始化完成,也可以直接用 muduo::CountDownLatch 到达相同的目的,将初值设为 1 即可。

    只要记住 Pthread 的条件变量是边沿触发(edge trigger),即 signal()/broadcast() 只会唤醒已经等在 wait() 上的线程(s),我们在编码时必须要考虑 signal() 早于 wait() 的可能,那么就很容易判断以下各个版本的正误了

    总结: 使用条件变量,调用 signal() 的时候无法知道是否已经有线程等待在 wait() 上。因此一般总是要先修改“条件”,使其为 true,再调用 signal();这样 wait 线程先检查“条件”,只有当条件不成立时才去 wait(),避免了丢事件的可能。换言之,通过使用“条件”,将边沿触发(edge trigger)改为电平触发(level trigger)。这里“修改条件”和“检查条件”都必须在 mutex 保护下进行,而且这个 mutex 必须用于配合 wait()。

    tips: 

    spurious wakeup?Wikipedia中是这样说的:

    Spurious wakeup describes a complication in the use of condition variables as provided by certain multithreading APIs such as POSIX Threads and the Windows API. Even after a condition variable appears to have been signaled from a waiting thread's point of view, the condition that was awaited may still be false. One of the reasons for this is a spurious wakeup; that is, a thread might be awoken from its waiting state even though no thread signaled the condition variable.

    spurious wakeup 指的是一次 signal() 调用唤醒两个或以上 wait()ing 的线程,或者没有调用 signal() 却有线程从 wait() 返回,虚假唤醒。

    APUE上这样说:

    POSIX规范为了简化实现,允许pthread_cond_signal在实现的时候可以唤醒不止一个线程。

    在发生的spurious wakeup时候,waiting线程被意外的唤醒,然后到真正signal的时候,waiting线程在之前已经spurious wakeup唤醒了。

    5、条件变量原理

    https://www.zhihu.com/question/24116967
     
    作者:马牛
    链接:https://www.zhihu.com/question/24116967/answer/26848581
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    有几篇30多年前的论文极大地影响了现代操作系统中进程/线程的同步机制的实现,尤其是楼主问题中的实现。一篇是 Monitors: An Operating System Structuring Concept ( ),还有一篇是 Experience with Processes and Monitors in Mesa ()。另外,还有讨论semaphore,生产者/消费者问题,哲学家就餐问题等等的论文。

    这里,我先介绍这两篇论文的内容,然后引出问题的答案。

    第一篇是Tony Hoare写的,通常被叫做Hoare's Monitor。你可能不知道Tony Hoare是谁,但是你应该知道他发明的quicksort。你也应该知道他得过的那个图灵奖。Monitor是一种用来同步对资源的访问的机制。Monitor里有一个或多个过程(procedure),在一个时刻只能有一个过程被激活(active)。让我来给个例子:
    MONITOR account {
        int balance; //initial value is 0;
        procedure add_one() {
            balance++
        }
        procedure remove_one() {
            balance--
        }
    }
    

    如果这是一个monitor,add_one()和remove_one()中只能有一个中被激活的。也就是说,balance++和balance--不可能同时执行,没有race。

    正如论文的标题所说的,monitor只是一个概念,他可以被几乎任何现代的语言实现。如果我们要用C++来实现Monitor,伪代码差不多就是这样(Java的Synchronization是Monitor的一个实现):
    class Account {
      private:
        int balance; //initial value is 0;
        lock mutex;
      public:
        void add_one() {
            pthread_mutex_lock(&mutex);
            balance++;
            pthread_mutex_unlock(&mutex);
        }
        void remove_one() {
            pthread_mutex_lock(&mutex);
            balance--
            pthread_mutex_unlock(&mutex);
        }
    };
    

    论文中也有条件变量(conditional variable),使用形式是cond_var.wait()和cond_var.signal()。让我们来看一下论文里最简单的一个例子。这是同步对单个资源访问的monitor。原文中的代码(好像)是用Pascal写的,我这里有C-style重写了一下。
    //注意这是一个monitor,这里的acquire()和release()不能也不会同时active。
    Monitor SingleResource {
      private:
        bool busy;
        condition_variable nonbusy;
      public:
        void acquire() {
            if ( busy == true ) {
                nonbusy.wait()
            }
            busy = true
        }
        void release() {
            busy = false;
            nonbusy.signal()
        }
        busy = false; //initial value of busy
    };
    
    需要特别注意,这是一个monitor(不是class)。其中的acquire()和release()不能也不会同时active。我们注意到,这里的nonbusy.wait()并没有使用lock作为参数。但是,Hoare其实是假设有的。只是在论文中,他把这个lock叫做monitor invariant。论文中,Hoare解释了为什么要在conditional wait时用到这个值(也就解释了楼主的问题)。我原文引用一下:
    Since other programs may invoke a monitor procedure during a wait, a waiting program must ensure that the invariant t for the monitor is true beforehand.

    换句话说,当线程A等待时,为了确保其他线程可以调用monitor的procedure,线程A在等待前,必须释放锁。例如,在使用monitor SingleResource时,线程A调用acquire()并等待。线程A必须在实际睡眠前释放锁,要不然,即使线程A已经不active了,线程B也没法调用acquire()。(当然,你也可以不释放锁,另一个线程根本不检查锁的状态,然后进入对条件变量的等待!! 但是,首先,这已经不是一个monitor了,其次,看下文。)

    pthread只是提供了一种同步的机制,你可以使用这种机制来做任何事情。有的对,有的错。Hoare在论文里的一段话也说更能解答楼主的问题:
    The introduction of condition variables makes it possible to write monitors subject to the risk of deadly embrace [7]. It is the responsibility of the programmer to avoid this risk, together with other scheduling disasters (thrashing, indefinitely repeated overtaking, etc. [11]).

    有兴趣的同学可以读读这篇文章,文中有一节专门解释了楼主的问题。楼主的问题显然是很深刻的。




    另外,为什么上面的代码里acquire()的实现使用的是:
    if ( busy == true ) {
    
    而不是:
    while ( busy == true ) {
    

    ?
    这是因为,在Hoare的假设里,当线程A调用nonbusy.signal()之后,线程A必须立即停止执行,正在等待的线程B必须紧接着立即开始执行。这样,就可以确保线程B开始执行时 busy==false。这正是我们想要的。

    但是,在现代的系统中,这个假设并不成立。现代操作系统中的机制跟Mesa中描述的一致:在condvar.signal()被调用之后,正在等待的线程并不需要立即开始执行。等待线程可以在任何方便的时候恢复执行(优点之一:这样就把同步机制和调度机制分开了)。

    在Mesa的假设下,上面的Monitor SingleResource的代码是错的。试想下面的执行过程:1. 线程A调用acquire()并等待,2. 线程B调用release(),2.线程C调用acquire(),现在busy=true,3. 线程A恢复执行,但是此时busy已经是true了! 这就会导致线程A和线程C同时使用被这个monitor保护的资源!!!
        void acquire() {
            if ( busy == true ) {
                nonbusy.wait()
            }
            //assert(busy != true)
            busy = true
        }
    

    在Mesa中,Butler Lampson和David Redell提出了一个简单的解决方案-把 if 改成 while。这样的话,在线程A恢复执行时,还要再检查一下busy的值。如果还不是想要的,就会再次等待。

    If you do not lock the mutex in the codepath that changes the condition and signals, you can lose wakeups. Consider this pair of processes:

    Process A:

    pthread_mutex_lock(&mutex);
    while (condition == FALSE)
        pthread_cond_wait(&cond, &mutex);
    pthread_mutex_unlock(&mutex);

    Process B (incorrect):

    condition = TRUE;
    pthread_cond_signal(&cond);

    Then consider this possible interleaving of instructions, where condition starts out as FALSE:

    Process A                             Process B
    
    pthread_mutex_lock(&mutex);
    while (condition == FALSE)
    
                                          condition = TRUE;
                                          pthread_cond_signal(&cond);
    
    pthread_cond_wait(&cond, &mutex);

    The condition is now TRUE, but Process A is stuck waiting on the condition variable - it missed the wakeup signal. If we alter Process B to lock the mutex:

    Process B (correct):

    pthread_mutex_lock(&mutex);
    condition = TRUE;
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);

    ...then the above cannot occur; the wakeup will never be missed.

    (Note that you can actually move the pthread_cond_signal() itself after the pthread_mutex_unlock(), but this can result in less optimal scheduling of threads, and you've necessarily locked the mutex already in this code path due to changing the condition itself).

  • 相关阅读:
    感悟.学习的方式
    IOS地理信息使用
    IOSTimer的例子留个备注
    IOSSelector的用法
    ios中addtarget的用法
    ios绘图时的坐标处理
    mac下装Ruby
    iOSbase64
    ios跳转
    Misha and Palindrome Degree CodeForces
  • 原文地址:https://www.cnblogs.com/ym65536/p/9503360.html
Copyright © 2011-2022 走看看