|
想必大家开发过程中都会用到多线程,用到多线程基本上都会用到条件变量,你理解的条件变量只是简单的wait和notify吗,最近工作中看同事也都只是简单的使用wait和notify,导致项目出现bug却不知如何fix bug,其实这里面还是有一些坑的,程序喵这里总结给大家。
6 i; P7 a. e: I- h2 Z 本文内容简介:
J, `# Q4 f, `" m0 `' R0 w: S) \" y; ~# e2 |
1 C4 R0 V. f0 S1 V3 I什么是条件变量? 条件变量如何使用? 如何解决条件变量的信号丢失问题? 如何解决条件变量的虚假唤醒问题? 8 y7 q$ Q+ r8 h, O* g
条件变量为什么一定要和锁配合使用?
/ h* f: V0 ^9 p+ f0 F. Z
/ R9 l* a1 W8 u % Z3 F( x8 Y. s1 x `: A7 N
/ h' u6 k3 h# G9 C4 [# u" y& Y4 z
1 ! B1 E- Z$ k+ W: N9 t$ ^) J
什么是条件变量?
& n a* z* v( Q. \
条件变量是多线程程序中用来实现等待和唤醒逻辑常用的方法。通常有wait和notify两个动作,wait用于阻塞挂起线程A,直到另一个线程B通过通过notify唤醒线程A,唤醒后线程A会继续运行。% [1 Y& \5 R4 p; |
条件变量在多线程中很常用,在有名的生产者和消费者问题中,消费者如何知道生成者是否生产出了可以消费的产品,通过while循环不停的去判断是否有可消费的产品?众所周知,死循环极其消耗CPU性能,所以需要使用条件变量来阻塞线程,降低CPU占用率。
- ? P x4 Q2 y, Q% K. F ! b9 \& C9 B8 ^
2
3 [; l7 D1 o3 ~条件变量的使用 拿生产者和消费者问题举例,看下面这段代码:- & I1 m1 V2 ^0 X: ]( S1 y
- std::mutex mutex;
: t2 m; J- L, s6 l+ L' g, E! q9 \ - std::condition_variable cv;
1 c( T, {. B: B - std::vector<int> vec;7 ?* J, t6 w }
3 S. e- `" H. n( T# v/ a- void Consume() {, i9 y% n. C- A0 q( _3 n% O# z8 Y
- std::unique_lock<std::mutex> lock(mutex);
( N! r+ K; h# |8 [# V8 ^ - cv.wait(lock);1 v9 h; J* K+ W/ A |" s
- std::cout << "consume " << vec.size() << "\n";1 _# L$ Y6 s! u$ v
- }
; _: D: m* Y2 T! A( K
' |$ Q2 {7 g5 ~, \& E0 I- |- void Produce() {
4 B+ i8 y# }4 n9 X3 H - std::unique_lock<std::mutex> lock(mutex);6 ]- L9 R9 b k, c
- vec.push_back(1);9 Z& Z# q3 U! m
- cv.notify_all();
0 {" \. @; k: s - std::cout << "produce \n";$ H3 t$ f# Q. [3 v% `5 _
- }
; D4 G, Z2 I9 Y: R8 ^( Q1 s
; H6 p0 w1 _" t' j' r+ M: w- int main() {6 `* z# f3 N! c9 B. z
- std::thread t(Consume);
}( R+ g7 c' [% f+ o - t.detach();
+ z- e' c3 B' \2 j s9 Y* q9 o- @ - Produce();
: w; H& c6 ~4 F' B0 U% x - return 0;3 Q& q) r( R0 z, O
- }
复制代码本意是消费者线程阻塞,等待生产者生产数据后去通知消费者线程,这样消费者线程就可以拿到数据去消费。 但这里有个问题: 如果先执行的Produce(),后执行的Consume(),生产者提前生产出了数据,去通知消费者,但是此时消费者线程如果还没有执行到wait语句,即线程还没有处于挂起等待状态,线程没有等待此条件变量上,那通知的信号就丢失了,后面Consume()中才执行wait处于等待状态,但此时生产者已经不会再触发notify,那消费者线程就会始终阻塞下去,出现bug。 如何解决这个问题呢?可以附加一个判断条件,就可以解决这种信号丢失问题,见代码:
3 s3 u! F* a8 y# f" A# Z
6 N% w4 h9 p1 r0 |' l- std::mutex mutex;
9 x' R r) V' O4 M3 s' d) L - std::condition_variable cv;3 L2 k; q5 G! l c7 Z
- std::vector<int> vec;
6 K$ \: N5 H) V0 |/ u, ]9 B3 v; ]
! J6 ^3 l* z& ]- d- void Consumer() {
1 s( o' I/ N$ L/ P+ {& P - std::unique_lock<std::mutex> lock(mutex);
5 T1 E8 C# T9 R - if (vec.empty()) { // 加入此判断条件
) c( r1 `0 ~' t! v* u. i" K - cv.wait(lock);
[" [9 j4 q. @% w$ u; M - }! v5 M2 L& A2 M \1 U1 Z4 a$ ?' U
- std::cout << "consumer " << vec.size() << "\n";
) D4 z; u$ C" O2 Y8 J$ D( o: b - }7 c. i; t; I3 s+ Y4 b1 {9 e$ D* f
$ g- P9 O$ Y, P- void Produce() {$ @& m' A; o8 x# L
- std::unique_lock<std::mutex> lock(mutex);! i1 V f4 P7 Y; ~- p4 k2 }- |
- vec.push_back(1);* O0 g+ b6 i/ X0 s1 ~
- cv.notify_all();" {6 n: H _0 t, ^+ }7 C
- std::cout << "produce \n";& E `7 f" a8 `5 u: G: O1 G
- }
* ]5 B% \" W* A" t! j
' k4 H" `$ L3 j- int main() {
" [$ t. d& r. \" m" J" {9 v - std::thread t(Consumer);
0 L$ l& q/ k9 k$ J4 Z. Z0 z - t.detach();4 f5 t3 A* ~& v" M4 o. W& Z: i
- Produce();, g0 C7 y$ ]- ^' x W
- return 0;) C1 ]$ ?3 c( f8 w
- }
复制代码通过增加附加条件可以解决信号丢失的问题,但这里还有个地方需要注意,消费者线程处于wait阻塞状态时,即使没有调用notify,操作系统也会有一些概率会唤醒处于阻塞的线程,使其继续执行下去,这就是虚假唤醒问题,当出现了虚假唤醒后,消费者线程继续执行,还是没有可以消费的数据,出现了bug。 % {/ f4 q `/ ^( P! Y9 s z9 w) @
那怎么解决虚假唤醒的问题呢,可以在线程由阻塞状态被唤醒后继续判断附加条件,看是否满足唤醒的条件,如果满足则继续执行,如果不满足,则继续去等待,体现在代码中,即将if判断改为while循环判断,见代码:
" \% c8 X7 ~! c) o
9 e+ C* C5 P# H- std::mutex mutex;+ K2 L* G5 [3 C! l
- std::condition_variable cv;+ K X9 v: ?; S3 w8 y- a
- std::vector<int> vec;
, S M/ \ Z3 `) s
' F! [' ?6 L, R, o- void Consumer() {
. p! g0 L2 R3 O; f$ \4 k+ R8 u$ E+ j - std::unique_lock<std::mutex> lock(mutex);
* \7 m' S' U: c' C7 `. f - while (vec.empty()) { // 将if改为while2 Z4 ?+ V% D! z# l1 Q h7 k/ x" W' v
- cv.wait(lock);2 o, Y( b! S5 z1 Z2 J
- }6 Z" C# F6 v/ v5 ~
- std::cout << "consumer " << vec.size() << "\n";; f: l% ]4 g a! Y% @& e' p7 O/ z
- }8 y# E7 u' Z) s& D2 ^2 y1 u; _5 m
: h7 g, X! j' B; |0 G5 Z1 o3 M- void Produce() {
6 P+ ~ v# a" D- T, Y% c1 F% M4 n - std::unique_lock<std::mutex> lock(mutex);
1 W3 m) x4 m1 s - vec.push_back(1);5 Q3 O2 D- t9 G* _
- cv.notify_all();3 Z2 u' I8 m _: n8 l
- std::cout << "produce \n";
5 w* E5 |: i& h& W# c - }1 w9 q8 h% n$ S( M4 s' J
7 E& T1 _. B- a2 s- int main() {0 I3 Q$ i' a: h; v& @8 {8 k5 J, |8 h
- std::thread t(Consumer);
; S6 X: @4 I" s" J - t.detach();) [! G9 b* L9 J% _. @
- Produce();& z- c x* Y+ i V4 Z2 v
- return 0; x) K( O* S8 |. X5 C
- }
复制代码看到这里相信你已经明白条件变量的使用啦,需要使用while循环附加判断条件来解决条件变量的信号丢失和虚假唤醒问题。( A! |6 d" X/ q
8 z5 E6 }0 @: I" B8 l4 a
2 V( r0 [: @* E- g+ [# g# E
3
3 v8 ~! }4 B% W# j }有没有更简单的“避坑”方式 9 v; [8 g/ ^; B. V. ?
难道我们每次都必须要使用while循环和附加条件来操作条件变量吗?这岂不是很麻烦?6 U: C6 B/ @4 U; c( T I
NO! ) b9 P+ Q8 \7 k% z- t3 F k+ v
在C++中其实有更好的封装,只需要调用wait函数时,在参数中直接添加附加条件就好了,内部已经做好了while循环判断,直接使用即可,见代码:
3 N% }/ W% Q3 E' t0 H - , a" V7 U, X! ?/ {: R. g1 g
- std::mutex mutex;1 P& w# n" c2 b4 `5 a
- std::condition_variable cv;
# N2 F, k0 C; a5 X3 J0 o* p9 l9 H8 E - std::vector<int> vec;
$ e$ G- V' y" ?! }
( T6 N c9 X' o& l! _) {- void Consumer() {
1 ]# B5 T+ p( \ P - std::unique_lock<std::mutex> lock(mutex);
. \; C3 y4 B" B2 O - cv.wait(lock, [&](){ return !vec.empty(); }); // 这里可以直接使用C++的封装7 C2 ]+ N! G$ Q9 s
- std::cout << "consumer " << vec.size() << "\n";+ @" Y4 W0 P/ ^; y" x, l2 T }/ G
- }
4 T9 i- C* m. t& v$ v - / L. k; t5 G# c
- void Produce() {
* }! F. m$ n" B+ J% A - std::unique_lock<std::mutex> lock(mutex);
$ L0 Y# A6 _- r - vec.push_back(1);
! P4 b) r q6 v9 }! w' K) c+ Z4 s - cv.notify_all();
5 u4 }0 A- w n+ D4 E9 { - std::cout << "produce \n";' [, z* v: H# H3 ^
- }
! p1 I" {1 a1 K! R. w, m2 _4 _ - 1 z+ O1 z% h- B, _/ u; q2 L
- int main() {
9 g4 k3 I {' K ^3 b - std::thread t(Consumer);% ?: O; Q( S. d" t# ?3 L
- t.detach();
1 c Z9 ?) j+ a) @- v& a* `3 b% N - Produce();
, i# a/ G d! p# E6 z0 [ - return 0;8 e" C& [2 t) ?" ^& N- `
- }
复制代码但在C语言中就没办法啦,大家只能自己做一层封装啦。 & H& A6 R7 t! `& t y
) K% F5 T$ P* f0 R2 G: A7 y
4
; Y% e! l: P- f" v8 |8 P8 F为什么条件变量需要和锁配合使用?
, W5 n7 ^# N9 \# B, U
- r W$ @6 C4 [* W
为什么叫条件变量呢?% s/ H8 j' {1 @- f
因为内部是通过判断及修改某个全局变量来决定线程的阻塞与唤醒,多线程操作同一个变量肯定需要加锁来使得线程安全。同时,一个简单的wait函数调用内部会很复杂的,有可能线程A调用了wait函数但是还没有进入到wait阻塞等待前,另一个线程B在此时却调用了notify函数,此时nofity的信号就丢失啦,如果加了锁,线程B必须等待线程A释放了锁并进入了等待状态后才可以调用notify,继而防止信号丢失。 关于条件变量就介绍到这里,希望大家能有所收获,平时使用过程中可以避掉条件变量的坑。
9 m8 Z- U* u, O" _7 p7 H
2 Y' ~" W7 }3 b$ y |