C++11 condition_variable

C++ Core Guidelines: Be Aware of the Traps of Condition Variables

C++11中condition_variable的陷阱

《C++ Concurrency In Action》读书笔记 - 线程间同步机制

condition_variable

What is condition_variable

std::condition_variable

Defined in header <condition_variable>
class condition_variable;  // (since C++11)

The condition_variable class is a synchronization primitive that can be used to block a thread, or multiple threads at the same time, until another thread both modifies a shared variable (the condition), and notifies the condition_variable.

The thread that intends to modify the variable has to

  1. acquire a std::mutex (typically via std::lock_guard)
  2. perform the modification while the lock is held
  3. execute notify_one or notify_all on the std::condition_variable (the lock does not need to be held for notification)

Even if the shared variable is atomic, it must be modified under the mutex in order to correctly publish the modification to the waiting thread.

Any thread that intends to wait on std::condition_variable has to

  1. acquire a std::unique_lock<std::mutex>, on the same mutex as used to protect the shared variable

  2. either

    1. check the condition, in case it was already updated and notified
    2. execute wait, wait_for, or wait_until. The wait operations atomically release the mutex and suspend the execution of the thread.
    3. When the condition variable is notified, a timeout expires, or a spurious wakeup occurs, the thread is awakened, and the mutex is atomically reacquired. The thread should then check the condition and resume waiting if the wake up was spurious.

    or

    1. use the predicated overload of wait, wait_for, and wait_until, which takes care of the three steps above.

    Why do we need condition_variable

    why do I need std::condition_variable?

    The purpose of std::condition_variable is to wait for some condition to become true. It is not designed to be just a receiver of a notify. You might use it, for example, when a consumer thread needs to wait for a queue to become non-empty.

    T get_from_queue() {
       std::unique_lock l(the_mutex);
       while (the_queue.empty()) {
         the_condition_variable.wait(l);
       }
       // the above loop is _exactly_ equivalent to the_condition_variable.wait(l, [&the_queue](){ return !the_queue.empty(); }
       // now we have the mutex and the invariant (that the_queue be non-empty) is true
       T retval = the_queue.top();
       the_queue.pop();
       return retval;
    }
    
    put_in_queue(T& v) {
      std::unique_lock l(the_mutex);
      the_queue.push(v);
      the_condition_variable.notify_one();  // the queue is non-empty now, so wake up one of the blocked consumers (if there is one) so they can retest.
    }
    

    The consumer (get_from_queue) is not waiting for the condition variable, they are waiting for the condition the_queue.empty(). The condition variable gives you the way to put them to sleep while they are waiting, simultaneously releasing the mutex and doing so in a way that avoids race conditions where you miss wake ups.

    Lost Wakeup and Spurious Wakeup

    唤醒丢失(Lost Wakeup)和虚假唤醒(Spurious Wakeup)

    • Lost wakeup: The phenomenon of the lost wakeup is that the sender sends its notification before the receiver gets to its wait state. The consequence is that the notification is lost. The C++ standard describes condition variables as a simultaneous synchronisation mechanism: "The condition_variable class is a synchronisation primitive that can be used to block a thread, or multiple threads at the same time, ...". So the notification gets lost, and the receiver is waiting and waiting and ... .

    • Spurious wakeup: It may happen that the receiver wakes up, although no notification happened. At a minimum POSIX Threads and the Windows API can be victims of these phenomena.

    • 唤醒丢失:在等待线程进入 wait 状态之前,发送线程就发送了通知。这种情况下,等待线程的 wait 再也收不到通知,只能死等。

    • 虚假唤醒:在某些平台(譬如 POSIX 或者 Windows 上),会产生虚假唤醒(原因未深究)。就是说,并不是发送线程发送的通知,等待线程确得到了通知。此时等待线程不再等待,而是继续执行下面的代码。我们前面说过,发送线程一般会做一些准备工作,这些准备工作是等待线程能够工作的前提条件。发送线程还没有发送通知,说明这些准备工作没有完成,此时等待线程就会面临无事可干的情况。

    How to solve this problem

    Example:

    // conditionVariables.cpp
    
    #include <condition_variable>
    #include <iostream>
    #include <thread>
    
    std::mutex mutex_;
    std::condition_variable condVar; 
    
    bool dataReady{false};
    
    void waitingForWork(){
        std::cout << "Waiting " << std::endl;
        std::unique_lock<std::mutex> lck(mutex_);
        condVar.wait(lck, []{ return dataReady; });   // (4)
        std::cout << "Running " << std::endl;
    }
    
    void setDataReady(){
        {
            std::lock_guard<std::mutex> lck(mutex_);
            dataReady = true;
        }
        std::cout << "Data prepared" << std::endl;
        condVar.notify_one();                        // (3)
    }
    
    int main(){
        
      std::cout << std::endl;
    
      std::thread t1(waitingForWork);               // (1)
      std::thread t2(setDataReady);                 // (2)
    
      t1.join();
      t2.join();
      
      std::cout << std::endl;
      
    }
    

    wait (lck, pred); 其实等价于 while (!pred()) wait(lck);,它的运行机制如下:

    1)线程获取 mutex 的锁,然后对 predicate 的结果进行检查:

    • true:线程继续往下执行;
    • falsecondVar.wait() 解锁 mutex,然后线程进入等待(阻塞)状态。

    2)假如 condVar 已经在等待状态,此时得到通知(不管发送线程发送,还是虚假唤醒):

    • 线程进入非阻塞状态,然后重新获取 mutex 的锁。
    • 线程检查 predicate 的结果:
      • true:线程继续往下执行;
      • falsecondVar.wait() 解锁 mutex,然后线程进入等待(阻塞)状态。

    那么上面的代码是如何解决唤醒丢失和虚假唤醒问题的呢?

    1)唤醒丢失也就是发送线程执行完了所有代码,然后发送通知之后,等待线程才走流程。此时发现 dataReady 已经为 true,等待线程可以继续往下执行。

    2)虚假唤醒假如先执行等待线程,并且等待线程检查 dataReady 为 false,则会进入阻塞状态。假设此时有虚假唤醒,等待线程首先进入非阻塞状态,重新获取 mutex 的锁,接下来它会去检查 dataReady 是否为 true。由于是虚假唤醒,说明发送线程并没有执行,dataReady 还是 false,此时 condVar.wait() 解锁 mutex,然后线程进入新一轮的等待(阻塞)状态,直至真正的唤醒到来。

    Can we use atomic instead of mutex?

    也许你已经注意到了,变量 dataReady 就是一个简单的 bool 型变量。我们能否让它变成 atomic 的变量,从而去除发送线程中的 mutex 呢?

    代码如下:

    // conditionVariableAtomic.cpp
    
    #include <atomic>
    #include <condition_variable>
    #include <iostream>
    #include <thread>
    
    std::mutex mutex_;
    std::condition_variable condVar;
    
    std::atomic<bool> dataReady{false};
    
    void waitingForWork(){
        std::cout << "Waiting " << std::endl;
        std::unique_lock<std::mutex> lck(mutex_);
        condVar.wait(lck, []{ return dataReady.load(); });   // (1)
        std::cout << "Running " << std::endl;
    }
    
    void setDataReady(){
        dataReady = true;
        std::cout << "Data prepared" << std::endl;
        condVar.notify_one();
    }
    
    int main(){
        
      std::cout << std::endl;
    
      std::thread t1(waitingForWork);
      std::thread t2(setDataReady);
    
      t1.join();
      t2.join();
      
      std::cout << std::endl;
      
    }
    

    上面的代码看起来像是避免了一次加锁,对性能有一定的优化。但是事实上,还是存在竞争条件(race condition),从而导致死锁。上面代码的行 (1) 等价与如下代码:

    std::unique_lock<std::mutex> lck(mutex_);
    while ( ![]{ return dataReady.load(); }() {
        // time window (1)
        condVar.wait(lck);
    }
    
    // condVar.wait(lck)
    lck.unlock()
    ...
    lck.lock()
    

    这里分析一下有 mutex 和没有 mutex 的保护的不同:

    1)没有 mutex:在上述代码中标注的 // time window (1) 处,也就是 condVar 还没有进入等待状态前,发送线程是可以执行完的,假如此时发送通知,通知会丢失。

    2)有 mutex:在 // time window (1) 时刻,等待线程持有 mutex 的锁,发送线程此时不能够获取锁,也就无法执行到 notify_one 函数。当等待线程进入等待状态后,线程释放锁,此时发送线程可以获取锁,修改 dataReady 的值并且调用 notify_one 函数发送通知。

posted @ 2020-07-06 20:25  IUNI_JM  阅读(380)  评论(0编辑  收藏  举报