c++11 条件变量 std::condition_variable,多线程同步

12 篇文章 5 订阅

一般来说,多线程中如果需要等待一个变量或者条件为true 或者同步多个线程,有两种方法:

1 . 忙等待,不停地检查该变量是否满足条件

while(pre)  // polling loop
{}

该方式有很多缺点:占用cpu资源,变量 pre 必须多线程安全,或者为 atomic 类型。在 while 中 sleep可以解决cpu占用问题,但是sleep不能在条件满足时及时的唤醒该线程。

2 . 使用条件变量 std::condition_variable

条件变量必须与 mutex 一起使用,mutex 的功能主要是保护共享数据,避免 race condition.

条件变量主要有这几个成员方法

// 存在虚假唤醒的可能,不推荐使用
void wait (unique_lock<mutex>& lck);  // 阻塞该线程,直到被唤醒

// 配合lamda函数,避免虚假唤醒——spurious wakeup
template <class Predicate>  // 阻塞该线程,并且只有条件变为 true 的时候才能被唤醒
  void wait (unique_lock<mutex>& lck, Predicate pred);
  
void notify_one();  // 唤醒一个等待的线程

void notify_all();  // 唤醒所有等待的线程

例如:

	std::condition_variable cv;  // 全局的
	std::mutex m; // 全局的
	...
	std::unique_lock<std::mutex> lk(m);
	
	// 阻塞,直到 my_flag == true 并且其他线程 notify 之后才返回
	cv.wait(lk, []{ return my_flag; }); // 等价于下面带 while循环的代码块
	...

wait 有两个功能:1 阻塞该线程的执行,2 调用 lc.unlock() ,使得其他被该锁阻塞的线程(如果有)可以获得 mutex,并执行;当wait返回时(被唤醒)该线程再次自动获得 mutex (即独占的获得锁),当lk离开作用域时,再次 lk.unlock(),然后其他被唤醒的线程(如果有)可以获得这个锁。

由于线程存在被虚假唤醒的可能,因此一般配合使用变量来防止被虚假唤醒,即使用带模板的 wait 函数: cv.wait(lk, [&]{ return my_flag == true; });
其实就是如下代码的封装:

	// 当虚假唤醒后,发现my_flag让然为false,继续调用wait
    while(!my_flag)
        cv.wait(lk);

所以条件变量都是如下三个要素一起用:

A condition_variable
A mutex
Some data guarded by the mutex

举例:

需要同步的区域可以用局部作用域来保护:

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>
 
std::condition_variable cv;
std::mutex cv_m; // This mutex is used for three purposes:
                 // 1) to synchronize accesses to i
                 // 2) to synchronize accesses to std::cerr
                 // 3) for the condition variable cv
int i = 0;
 
void waits()
{
	// {  // 建立一个局部作用域
    std::unique_lock<std::mutex> lk(cv_m);
    std::cerr << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    // }
    std::cerr << "...finished waiting. i == 1\n";  // 如果这一句不需要同步,则可以去掉上面两个注释,即将mutex的保护范围缩小。
}
 
void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {   
	    // 若不需要同步 std::cerr 可以去掉该 lock_guard
        std::lock_guard<std::mutex> lk(cv_m);
        std::cerr << "Notifying...\n";
    }
    
    cv.notify_all();  // 唤醒所有等待cv的线程,但是由于 i!=1 所以不能真正唤醒。
 
    std::this_thread::sleep_for(std::chrono::seconds(1));
 
    {
        std::lock_guard<std::mutex> lk(cv_m);
        i = 1;  // 满足条件
        std::cerr << "Notifying again...\n";
    }
    cv.notify_all();  // 再次唤醒所有线程,然后各个线程依次执行。
}
 
int main()
{
    std::thread t1(waits), t2(waits), t3(waits), t4(signals);
    t1.join(); 
    t2.join(); 
    t3.join();
    t4.join();
}

输出:

Waiting... 
Waiting... 
Waiting... 
Notifying...
Notifying again...
...finished waiting. i == 1
...finished waiting. i == 1
...finished waiting. i == 1

总结:

调用 wait 之前必须获得锁:

The converse of this tip, i.e., hold the lock when calling wait, is not just
a tip, but rather mandated by the semantics of wait, because wait always
(a) assumes the lock is held when you call it, (b) releases said lock when
putting the caller to sleep, and © re-acquires the lock just before returning.
Thus, the generalization of this tip is correct: hold the lock when
calling signal or wait, and you will always be in good shape.


c++11 引入了内存模型,除了使用 mutex,还提供了更为高效的 atomic<T> 类型。
关于内存模型,参见 link, 写的通俗易懂,举例到位。
关于内存模型,还可以了解一下 c++ 的 type-punning 问题, see link.

other ref links:
http://en.cppreference.com/w/cpp/thread/condition_variable/notify_all
http://www.cplusplus.com/reference/condition_variable/condition_variable/wait/
http://stackoverflow.com/questions/16350473/why-do-i-need-stdcondition-variable
http://blog.csdn.net/gw569453350game/article/details/51577319
http://blog.csdn.net/gw569453350game/article/details/51564925
http://pages.cs.wisc.edu/~remzi/OSTEP/threads-cv.pdf
http://stackoverflow.com/questions/32978066/why-is-there-no-wait-function-for-condition-variable-which-does-not-relock-the-m

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值