c++11多线程编程同步——使用条件变量condition variable

简述


在多线程编程中,当多个线程之间需要进行某些同步机制时,如某个线程的执行需要另一个线程完成后才能进行,可以使用条件变量。

c++11提供的 condition_variable 类是一个同步原语,它能够阻塞一个或者多个线程,直到另一线程修改共享变量并通知 condition_variable。

也可以把它理解为信号通知机制,一个线程负责发送信号,其他线程等待该信号的触发。

condition_variable 存在一些问题,如虚假唤醒,这可以通知增加额外的共享变量来避免。

针对增加额外的变量这一点,为什么不在另一线程循环检测这个变量,从而达到相同目的而不需要再使用条件变量?

  • 循环检测时,程序在高速运行,占用过高的cpu,而条件变量的等待是阻塞,休眠状态下cpu使用率为0,省电!
  • 对于运行中的线程,可能会被操作系统调度,切换cpu核心,这样一来,所有的缓存可能失效,而条件变量不会,省时!

对于只需要通知一次的情况,如初始化完成、登录成功等,建议不要使用 condition_variable,使用std::future更好。

使用


通知方:

  • 获取 std::mutex, 通常是 std::lock_guard
  • 修改共享变量(即使共享变量是原子变量,也需要在互斥对象内进行修改,以保证正确地将修改发布到等待线程)
  • 在 condition_variable 上执行 notify_one/notify_all 通知条件变量(该操作不需要锁)

等待方:

  • 获取相同的 std::mutex, 使用 std::unique_lock
  • 执行 wait,wait_for或wait_until(该操作会自动释放锁并阻塞)
  • 接收到条件变量通知、超时或者发生虚假唤醒时,线程被唤醒,并自动获取锁。唤醒的线程负责检查共享变量,如果是虚假唤醒,则应继续等待

std :: condition_variable仅适用于 std::unique_lock, 此限制允许在某些平台上获得最大效率。 std :: condition_variable_any提供可与任何BasicLockable对象一起使用的条件变量,例如std :: shared_lock。

示例代码如下:

#include 
#include 
#include 
#include 
#include 
 
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
 
void worker_thread()
{
    // Wait until main() sends data
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{return ready;});
 
    // after the wait, we own the lock.
    std::cout << "Worker thread is processing data\n";
    data += " after processing";
 
    // Send data back to main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
 
    // Manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    lk.unlock();
    cv.notify_one();
}
 
int main()
{
    std::thread worker(worker_thread);
 
    data = "Example data";
    // send data to the worker thread
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
 
    // wait for the worker
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';
 
    worker.join();
}

输出:

main() signals data ready for processing
Worker thread is processing data
Worker thread signals data processing completed
Back in main(), data = Example data after processing

其中:

  • notify_one() 只唤醒一个线程,如果有多个线程,具体唤醒哪一个不确定,如果需要唤醒其他所有线程,使用 notify_all()
  • 执行 notify_one() 时不需要锁
  • 修改共享变量 ready/processed 时需要锁,共享变量用于避免虚假唤醒
  • cv.wait 第一个参数必须是 unique_lock,因为它内部会执行 unlock和lock,如果需要设置超时,使用 wait_for/wait_until

总结


使用注意事项:

  • 需要共享变量来避免虚假唤醒
  • 共享变量的修改需要在锁内进行
  • 通知线程在发出通知时不需要加锁
  • 通知线程一般使用lock_guard即可
  • 接收线程的wait函数第一个参数需要是unique_lock
  • 接收线程需要判断是否为虚假唤醒

特别注意,如果不使用共享变量,当通知线程在接收线程准备接收之前发送通知,接收线程将要永远阻塞了。这里,共享变量已经置位,所以它也能避免丢失唤醒。

总之,条件变量使用广泛,了解它的优势和不足,便于我们作出最佳决策。

参考资料

std::condition_variable
why do I need std::condition_variable?

你可能感兴趣的:(cpp)