条件变量

We’re going to make you an offer you can’t refuse.

简介

在多线程并发的程序中,我们有互斥锁的机制保证线程同步,但在有些情况下,仅仅依靠互斥锁是不够的,我们有时还会遇到下面的情况:令一个线程等待,直到特殊情况发生为止。例如,我们希望父线程在继续执行前检查子线程是否完成(join函数),那么我们如何实现这个功能呢?一个简单的例子是使用一个volatile的全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
volatile int done = 0;

void *child(void *arg) {
printf("child\n");
done = 1;
return NULL;
}

int main(int argc, char *argv[]) {
printf("parent: begin\n");
pthread_t c;
Pthread_create(&c, NULL, child, NULL); // create child
while (done == 0) //循环直到child改变done
; // spin
printf("parent: end\n");
return 0;
}

条件变量

条件变量定义

条件变量是多线程程序中用来实现“等待—>唤醒”机制的手段,以两个线程为例,通常我们会执行如下动作:

  • 线程1等待条件变量指定的条件成立,如果不成立,则挂起
  • 线程2使条件成立,从而使线程1从挂起状态转移至执行状态

通常,条件变量指定的条件是一个全局变量,这样两个线程都可以进行访问,但是我们必须采用互斥锁对变量上锁,避免发生访问冲突。

条件变量应用场景

  • 生产者消费者问题
  • 读写问题中用于切换读写状态

条件变量使用注意

虚假唤醒(spurious wakeup)

虚假唤醒会影响程序的性能,有几种情况可能导致不满足条件的情况下,线程被唤醒:

The pthread_cond_broadcast() function shall unblock all threads currently blocked on the specified condition variable cond.

  • 没有signal但是线程就是被唤醒了

  • 线程被唤醒,但是得不到锁,只能继续等待

1
2
3
4
5
6
7
8
9
// Writer
Lock(mutex)
resource_counter = 0
Broadcast(read_phase)
Signal(write_phase)
Unlock(mutex)

// Reader
Wait(mutex, read_phase)

在上面这个代码中,Writer广播了条件,一个reader接收到条件被唤醒,但是如果Writer此时没有释放锁,那么Reader只能重新等待。一个解决方法是将Broadcast和Signal提到外边,但是这个只在本例中适用。

使用注意

  • 不要忘记提醒等待的线程
  • 如果不确定应该单播还是广播,就使用广播,虽然会影响性能

C++11中的条件变量

创建条件变量

C++11中的条件变量创建方法如下:

1
std::condition_variable cond;//条件变量

其中,condition_variable是一个数据结构,保存了锁、等待的线程等关键信息

wait方法

wait会使当前线程阻塞,直到达到某种特定的条件将线程唤醒。其通用的API定义为:

1
wait(mutex, cond)

使用wait方法时,我们必须保证调用wait的线程释放了mutex,然后等wait被唤醒后又自动重新上锁。wait方法的逻辑如下:

1
2
3
4
5
6
7
wait(mutex, cond){
//自动释放锁然后进入等待队列

//等待

//从等待队列中取出,然后重新获得锁
}

C++ wait实现

在C++11中,其函数定义如下:

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

该函数包含两个参数,含义如下:

  • lock - 一个std::unique_lock<std::mutex>对象,使用前必须上锁
  • pred - 一个返回bool类型的函数,如果返回false,代表线程需要继续挂起,返回true代表可以执行

wait(两参数)的实现如下

1
2
3
4
5
6
7
template<typename _Predicate>
void
wait(unique_lock<mutex>& __lock, _Predicate __p)
{
while (!__p())
wait(__lock);
}

可以看到wait函数逻辑很简单,如果条件不满足,就一直等待。而一个参数的wait实际是调用了pthread_cond_wait,这个函数的详细描述见参考文献1

signal方法

唤醒一个/所有正在等待某个条件的线程,其通用API定义如下

1
2
signal(cond)    //唤醒一个特定的线程
broadcast(cond) //唤醒所有等待cond的线程

在C++中,signal方法定义为notify_one()notify_all()

条件变量使用

条件变量和锁控制临界区框架

这里提供一个典型的使用条件变量和锁对临界区进行控制的框架

1
2
3
4
5
6
7
8
9
10
11
Lock(mutex)
// 等待
while(!predicate)
wait(mutex, cond_var)

// 更新状态
update state and predicate

// 发送/广播信号
signal and/or broadcast
Unlock(mutex)

C++多线程交替打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <bits/stdc++.h>
#include <mutex>
#include <thread>
#include <condition_variable>
using namespace std;

std::mutex lock; //互斥锁
std::condition_variable cv;//条件变量
bool flag = true;
void printA() {
int i = 1;
while(i <= 100) {
std::unique_lock<std::mutex> lck(lock);
cv.wait(lck,[]{return flag;}); //等待flag==true才打印奇数
std::cout<<"A " << i <<endl;
i += 2;
flag = false;
cv.notify_one(); //唤醒线程B,cv.wait 被唤醒,检查flag,发现可以执行
}
}

void printB() {
int i = 2;
while(i <= 100) {
std::unique_lock<std::mutex> lck(lock);
cv.wait(lck,[]{return !flag;}); //等待flag==false才打印偶数
std::cout<<"B " << i <<endl;
i += 2;
flag = true;
cv.notify_one();
}
}
int main() {
std::thread tA(printfA);
std::thread tB(printfB);
tA.join();
tB.join();
return 0;
}

参考文献

0%