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

一般来说,多线程中如果需要等待一个变量或者条件为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& lck);  // 阻塞该线程,直到被唤醒

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

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

例如:

	std::condition_variable cv;  // 全局的
	std::mutex m; // 全局的
	...
	std::unique_lock 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 
#include 
#include 
#include 
 
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 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 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 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 类型。
关于内存模型,参见 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

你可能感兴趣的:(c/c++,orocos)