文章目录
  1. 1. C++11之多线程系列文章目录
  2. 2. 条件变量(Condition Variable)
    1. 2.1. 条件变量为什么叫条件变量?
    2. 2.2. 为什么条件变量需要和锁一起使用?

C++11之多线程系列文章目录

一、标准库的线程封装类Thread和Future
二、互斥对象(Mutex)和锁(Lock)
三、条件变量(Condition Variable)
四、原子操作(Atomic Operation)
五、内存序(Memory Order)

条件变量(Condition Variable)

条件变量是一种同步原语(Synchronization Primitive)用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程直到:

  • 收到来自其他线程的通知
  • 超时
  • 发生虚假唤醒(Spurious Wakeup)

C++11为条件变量提供了两个类

  • std::condition_variable:必须与std::unique_lock配合使用
  • std::condition_variable_any:更加通用的条件变量,可以与任意类型的锁配合使用,相比前者使用时会有额外的开销

二者具有相同的成员函数

成员函数 说明
notify_one 通知一个等待线程
notify_all 通知全部等待线程
wait 阻塞当前线程直到被唤醒
wait_for 阻塞当前线程直到被唤醒或超过指定的等待时间(长度)
wait_until 阻塞当前线程直到被唤醒或到达指定的时间(点)

二者在线程要等待条件变量前都必须要获得相应的锁

条件变量为什么叫条件变量?

  • 条件变量存在虚假唤醒的情况,因此在线程被唤醒后需要检查条件是否满足
  • 无论是notify_one或notify_all都是类似于发出脉冲信号,如果对wait的调用发生在notify之后是不会被唤醒的,所以接收者在使用wait等待之前也需要检查条件(标识)是否满足,另一个线程(通知者)在nofity前需要修改相应标识供接收者检查

条件变量因此得名。

为什么条件变量需要和锁一起使用?

观察std::condition_variable::wait函数,发现它的两个重载都必须将锁作为参数

1
2
3
void wait(std::unique_lock<std::mutex>& lock);
template< class Predicate >
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

首先考虑wait函数不需要锁作为参数的情况,下面的代码中flag初始化为false,线程A将flag置为true并使用notify_one发出通知,线程B使用while循环在wait前后都会检查flag,直到flag被置为true才往下执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Thread A
{
std::unique_lock lck(mt);
flag = true;
}
cv.notify_one();

// Thread B
auto pred = []() {
std::unique_lock lck(mt);
return flag;
};
while(!pred()) {
cv.wait();
}

如果两个线程的执行顺序为:

  1. 线程B检查flag发现其值为false
  2. 线程A将flag置为true
  3. 线程A使用notify_one发出通知
  4. 线程B使用wait进行等待

那么线程B将不会被唤醒(即线程B没有察觉到线程A发出的通知),这显然不是程序员想要的结果,发生这种情况的根源在于线程B对条件的检查和进入等待的中间是有空档的。wait函数需要锁作为参数正是为了解决这一问题的。

1
2
3
4
5
6
7
8
// Thread B
auto pred = []() {
return flag;
};
std::unique_lock lck(mt);
while(!pred()) {
cv.wait(lck);
}

当线程B调用wait的时候会释放传入的锁并同时进入等待,当被唤醒时会重新获得锁,因此只要保证线程A在修改flag的时候是正确加锁的那么就不会发生前面的这种情况。
使用wait函数的另一个重载时下面的代码与上面的6~8行是等价的。

1
cv.wait(lck, pred);

不仅仅是C++,就博主所知道的语言但凡有条件变量的概念都必须与锁配合使用。以C#、Java为例

C#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Thread A
lock (obj)
{
flag = true;
System.Threading.Monitor.Pulse(obj);
}

// Thread B
lock (obj)
{
while(!pred())
{
System.Threading.Monitor.Wait(obj);
}
}

Java

1
2
3
4
5
6
7
8
9
10
11
12
// Thread A
synchronized(obj) {
flag = true;
obj.notify();
}

// Thread B
synchronized(obj) {
while(!pred()) {
obj.wait();
}
}

C#与C++不同之处在于C#在Pulse或PulseAll的线程必须持有锁,而C++的notify_one和notify_all则无所谓是否持有锁。

文章目录
  1. 1. C++11之多线程系列文章目录
  2. 2. 条件变量(Condition Variable)
    1. 2.1. 条件变量为什么叫条件变量?
    2. 2.2. 为什么条件变量需要和锁一起使用?