条件变量

这里不讲条件变量 std::conditional_varibale 的具体使用,自行上 cppreference 网站查阅手册。

条件变量

std::condition_variable 类型描述

在 pthread 中条件变量的惯用手法是将 wait 放在 while 循环中。在 C++11 之后,常见的书写形式为

1
2
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return ready;});

其完全等价于循环方式,而将 wait 放到循环中,主要是因为虚假唤醒的存在。

此重载可用于在等待特定条件成为 true 时忽略虚假唤醒。

初次之外,还有 wait_forwait_util

需要强调的是在循环中使用 wait_forwait_for 带谓词的重载并不等价,是有显著区别的,甚至于在循环中调用 wait_for 有造成永久阻塞的风险(处理不了虚假唤醒)。

wait_for 阻塞当前线程,直到条件变量被唤醒,或到指定时限时长后

wait_for 带有谓词的重载由 wait_util 实现,可用于忽略虚假唤醒。

How does condition_variable::wait_for() deal with spurious wakeups?

在循环中使用 wait_until 和其带有谓词的重载完全等价,都可用于忽略虚假唤醒。

wait_until 阻塞当前线程,直到条件变量被唤醒,或直到抵达指定时间点

条件变量 wait 参数锁住的到底是什么

以下文字摘抄自 std::condition_variable,其中语义不通或翻译欠妥的地方直接改换为英文原文了。

有意修改变量(条件)的线程必须

  1. 获得 std::mutex (典型地通过 std::lock_guard
  2. 在保有锁时进行修改
  3. std::condition_variable 上执行 notify_one notify_all 不需要为通知保有锁

即使共享变量是原子的,也必须在互斥下修改它,以正确地发布修改到等待的线程。- why

任何有意在 std::condition_variable 上等待的线程必须

  1. 获得 std::unique_lock<std::mutex> ,on the same mutex as used to protect the shared variable
  2. 执行 wait wait_for wait_until ,The wait operations 自动释放互斥,并悬挂线程的执行
  3. condition_variable 被通知时,时限消失或虚假唤醒发生,线程被唤醒,且自动重获得互斥。之后线程应检查条件,若唤醒是虚假的,则继续等待。

所以,_lock 是针对共享变量(条件),wait() 使用 _lock 是为了悬挂线程执行时释放互斥的所有权以及跳出等待时线程再次获得互斥的所有权——如果由用户 unlock、lock 显然会造成竞争,无法保证原子性

条件变量 notify_one 之前为什么要加锁?不加锁为什么卡死?

std::condition_variable 中提到:

不需要为通知保有锁

另外 std::condition_variable::notify_one 示例,如果 在通知前依旧保有锁 通知线程一直持有锁,会造成被通知线程持续阻塞,进而通知线程循环无法结束……无解。

那么,标题描述的情形是否是由其他原因造成的呢?即便必须加锁,也是业务原因而非 condition_variable::notify 使用方式

结论

条件变量 wait() 时用锁:

  • notify() 时可以不保有锁,但修改共享变量(条件/谓词)时必须加锁
  • 如果修改谓词时不加锁,那么 notify() 之前必须加锁(以保证要么 wait() 块未进入/尚未执行谓词判断,要么 wait() 已在 sleep )。这样子有个缺点,需要注意被通知线程才被唤醒就阻塞,所以在 notify() 之后应迅速释放锁

如果修改谓词和 notify() 的时候都不加锁,就有概率发生丢失 notify() 事件(多线程竞争,wait() 发生在 notify() 之后,被通知线程一直阻塞下去)

分析过程

notify_one()/notify_all() 的效果与 wait()/wait_for()/wait_until() 的三个原子部分的每一者(解锁+等待、唤醒和锁定)以能看做原子变量修改顺序单独全序发生。

wait() 操作的三个原子部分:

  1. unlock+wait
  2. wakeup
  3. lock

通知时不保有锁,会不会有问题?

If it is possible for another thread to change the value of the predicate while this thread holds the mutex, then it is possible for notifications to occur between the predicate check and going to sleep, and effectively be lost.

——上述表述的既定前提:“ predicate 发生变化后 notify(),随意 notify() 的情形没有讨论意义”

1
2
3
4
5
while (!pred()) {
// if notify() happens? sleep forever?
// hold mutex
wait(lock);
}

Holding the mutex at any time between the change to the predicate and the notification is sufficient to guarantee the atomicity of the predicate check and wait call in the other thread.

  • 被通知线程先拿到锁:因为 unlock+wait 的原子性,所以通知线程修改 predicate/ notify() 之前已经 wait
  • 通知线程先拿到锁:那么在 predicate 改变之后,被通知线程 check 为假,直接跳过了 wait() 操作

线程对象

程序退出时,条件变量等待~有的会退,有的卡住

DZHYUNRequest.cpp: L442

1
2
3
4
5
6
std::unique_lock<std::mutex> _locker_normal(g_mutex_normal);
while (result != 0 && get_ws_con_status() != WSCLIENT_STATUS::Open)
{
g_check_normal.wait(_locker_normal);
result = _client.send(_cmid, reqmsg + qids.str() + "&token=" + _token);
}

能够退出

MainComponent.cpp: L154

1
2
3
4
5
6
7
8
9
10
11
12
std::unique_lock<std::mutex> _locker_normal(g_mutex_normal);
while (isNormal == false)
{
g_check_normal.wait(_locker_normal);
isNormal = (WSCLIENT_STATUS::Open == YDdata_GetWSConStatus());
if (isNormal)
{
_locker_normal.unlock();
// 连接成功提示
InfoBox("Open");
}
}

不能退出

其中的差别在哪里?前者使用的 boost::thread,后者使用的 std::thread。前者使用了 boost 线程库的 interrupt 机制

std::thread 线程类

std::thread 类型描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::thread t1, t2;	// 构造不表示线程的新 thread 对象

std::thread t3([]() {
for (size_t i = 0; i < 10; i++) {
this_thread::sleep_for(200ms);
}
});
t3.swap(t2); // 互换二个 thread 对象的底层句柄。
// 和空的线程对象互换也是可以的,但注意之后对 t2 调用 join() 或者 detach()

if (t1.joinable())
t1.join();
if (t2.joinable())
t2.join(); // 不 join 也不 detach 就会崩溃
if (t3.joinable())
t3.join();

线程对象的析构函数

std::thread::~thread	// 销毁 thread 对象。

析构时,若 *this 拥有关联线程( joinable() == true ),则调用 std::terminate()

注意:在下列操作后 thread 对象无关联的线程(从而可安全销毁)

  • 被默认构造
  • 被移动
  • 已调用 join()
  • 已调用 detach()

和线程对象有关,触发的崩溃,一般都是因为没有合理处理 joinable() 造成的。

线程和引用

1
2
3
4
5
6
7
8
for (size_t i = 0; i < 100; i++)
{
const string label = std::to_string(i);
// vt.push_back(thread([label]() { // it's ok
vt.push_back(thread([&]() {
cout << label << endl;
}));
}

输出乱序!不使用 & 引用输出结果正确。