条件变量
这里不讲条件变量 std::conditional_varibale
的具体使用,自行上 cppreference 网站查阅手册。
条件变量
在 pthread 中条件变量的惯用手法是将 wait
放在 while
循环中。在 C++11 之后,常见的书写形式为
1 | std::unique_lock<std::mutex> lk(m); |
其完全等价于循环方式,而将 wait
放到循环中,主要是因为虚假唤醒的存在。
此重载可用于在等待特定条件成为 true 时忽略虚假唤醒。
初次之外,还有 wait_for
和 wait_util
。
需要强调的是在循环中使用 wait_for
和 wait_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,其中语义不通或翻译欠妥的地方直接改换为英文原文了。
有意修改变量(条件)的线程必须
- 获得
std::mutex
(典型地通过std::lock_guard
) - 在保有锁时进行修改
- 在
std::condition_variable
上执行notify_one
或notify_all
(不需要为通知保有锁)
即使共享变量是原子的,也必须在互斥下修改它,以正确地发布修改到等待的线程。- why
任何有意在 std::condition_variable
上等待的线程必须
- 获得
std::unique_lock<std::mutex>
,on the same mutex as used to protect the shared variable - 执行
wait
、wait_for
或wait_until
,The wait operations 自动释放互斥,并悬挂线程的执行。 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() 操作的三个原子部分:
- unlock+wait
- wakeup
- 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 | while (!pred()) { |
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 andwait
call in the other thread.
- 被通知线程先拿到锁:因为 unlock+wait 的原子性,所以通知线程修改 predicate/
notify()
之前已经 wait - 通知线程先拿到锁:那么在 predicate 改变之后,被通知线程 check 为假,直接跳过了
wait()
操作
线程对象
程序退出时,条件变量等待~有的会退,有的卡住
DZHYUNRequest.cpp: L442
1 | std::unique_lock<std::mutex> _locker_normal(g_mutex_normal); |
能够退出
MainComponent.cpp: L154
1 | std::unique_lock<std::mutex> _locker_normal(g_mutex_normal); |
不能退出
其中的差别在哪里?前者使用的 boost::thread
,后者使用的 std::thread
。前者使用了 boost 线程库的 interrupt 机制。
std::thread 线程类
1 | std::thread t1, t2; // 构造不表示线程的新 thread 对象 |
线程对象的析构函数
std::thread::~thread // 销毁 thread 对象。
析构时,若 *this
拥有关联线程( joinable() == true
),则调用 std::terminate()
。
注意:在下列操作后 thread 对象无关联的线程(从而可安全销毁)
- 被默认构造
- 被移动
- 已调用
join()
- 已调用
detach()
和线程对象有关,触发的崩溃,一般都是因为没有合理处理 joinable()
造成的。
线程和引用
1 | for (size_t i = 0; i < 100; i++) |
输出乱序!不使用 &
引用输出结果正确。