你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

条件变量简析

[复制链接]
gaosmile 发布时间:2020-8-28 21:53
想必大家开发过程中都会用到多线程,用到多线程基本上都会用到条件变量,你理解的条件变量只是简单的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 ~
条件变量的使用
拿生产者和消费者问题举例,看下面这段代码:
  1. & I1 m1 V2 ^0 X: ]( S1 y
  2. std::mutex mutex;
    : t2 m; J- L, s6 l+ L' g, E! q9 \
  3. std::condition_variable cv;
    1 c( T, {. B: B
  4. std::vector<int> vec;7 ?* J, t6 w  }

  5. 3 S. e- `" H. n( T# v/ a
  6. void Consume() {, i9 y% n. C- A0 q( _3 n% O# z8 Y
  7.   std::unique_lock<std::mutex> lock(mutex);
    ( N! r+ K; h# |8 [# V8 ^
  8.   cv.wait(lock);1 v9 h; J* K+ W/ A  |" s
  9.   std::cout << "consume " << vec.size() << "\n";1 _# L$ Y6 s! u$ v
  10. }
    ; _: D: m* Y2 T! A( K

  11. ' |$ Q2 {7 g5 ~, \& E0 I- |
  12. void Produce() {
    4 B+ i8 y# }4 n9 X3 H
  13.   std::unique_lock<std::mutex> lock(mutex);6 ]- L9 R9 b  k, c
  14.   vec.push_back(1);9 Z& Z# q3 U! m
  15.   cv.notify_all();
    0 {" \. @; k: s
  16.   std::cout << "produce \n";$ H3 t$ f# Q. [3 v% `5 _
  17. }
    ; D4 G, Z2 I9 Y: R8 ^( Q1 s

  18. ; H6 p0 w1 _" t' j' r+ M: w
  19. int main() {6 `* z# f3 N! c9 B. z
  20.   std::thread t(Consume);
      }( R+ g7 c' [% f+ o
  21.   t.detach();
    + z- e' c3 B' \2 j  s9 Y* q9 o- @
  22.   Produce();
    : w; H& c6 ~4 F' B0 U% x
  23.   return 0;3 Q& q) r( R0 z, O
  24. }
复制代码
本意是消费者线程阻塞,等待生产者生产数据后去通知消费者线程,这样消费者线程就可以拿到数据去消费。
但这里有个问题:
如果先执行的Produce(),后执行的Consume(),生产者提前生产出了数据,去通知消费者,但是此时消费者线程如果还没有执行到wait语句,即线程还没有处于挂起等待状态,线程没有等待此条件变量上,那通知的信号就丢失了,后面Consume()中才执行wait处于等待状态,但此时生产者已经不会再触发notify,那消费者线程就会始终阻塞下去,出现bug。
如何解决这个问题呢?可以附加一个判断条件,就可以解决这种信号丢失问题,见代码:
3 s3 u! F* a8 y# f" A# Z

  1. 6 N% w4 h9 p1 r0 |' l
  2. std::mutex mutex;
    9 x' R  r) V' O4 M3 s' d) L
  3. std::condition_variable cv;3 L2 k; q5 G! l  c7 Z
  4. std::vector<int> vec;
    6 K$ \: N5 H) V0 |/ u, ]9 B3 v; ]

  5. ! J6 ^3 l* z& ]- d
  6. void Consumer() {
    1 s( o' I/ N$ L/ P+ {& P
  7.   std::unique_lock<std::mutex> lock(mutex);
    5 T1 E8 C# T9 R
  8.   if (vec.empty()) { // 加入此判断条件
    ) c( r1 `0 ~' t! v* u. i" K
  9.       cv.wait(lock);
      [" [9 j4 q. @% w$ u; M
  10.   }! v5 M2 L& A2 M  \1 U1 Z4 a$ ?' U
  11.   std::cout << "consumer " << vec.size() << "\n";
    ) D4 z; u$ C" O2 Y8 J$ D( o: b
  12. }7 c. i; t; I3 s+ Y4 b1 {9 e$ D* f

  13. $ g- P9 O$ Y, P
  14. void Produce() {$ @& m' A; o8 x# L
  15.   std::unique_lock<std::mutex> lock(mutex);! i1 V  f4 P7 Y; ~- p4 k2 }- |
  16.   vec.push_back(1);* O0 g+ b6 i/ X0 s1 ~
  17.   cv.notify_all();" {6 n: H  _0 t, ^+ }7 C
  18.   std::cout << "produce \n";& E  `7 f" a8 `5 u: G: O1 G
  19. }
    * ]5 B% \" W* A" t! j

  20. ' k4 H" `$ L3 j
  21. int main() {
    " [$ t. d& r. \" m" J" {9 v
  22.   std::thread t(Consumer);
    0 L$ l& q/ k9 k$ J4 Z. Z0 z
  23.   t.detach();4 f5 t3 A* ~& v" M4 o. W& Z: i
  24.   Produce();, g0 C7 y$ ]- ^' x  W
  25.   return 0;) C1 ]$ ?3 c( f8 w
  26. }
复制代码
通过增加附加条件可以解决信号丢失的问题,但这里还有个地方需要注意,消费者线程处于wait阻塞状态时,即使没有调用notify,操作系统也会有一些概率会唤醒处于阻塞的线程,使其继续执行下去,这就是虚假唤醒问题,当出现了虚假唤醒后,消费者线程继续执行,还是没有可以消费的数据,出现了bug。  % {/ f4 q  `/ ^( P! Y9 s  z9 w) @
那怎么解决虚假唤醒的问题呢,可以在线程由阻塞状态被唤醒后继续判断附加条件,看是否满足唤醒的条件,如果满足则继续执行,如果不满足,则继续去等待,体现在代码中,即将if判断改为while循环判断,见代码:
" \% c8 X7 ~! c) o

  1. 9 e+ C* C5 P# H
  2. std::mutex mutex;+ K2 L* G5 [3 C! l
  3. std::condition_variable cv;+ K  X9 v: ?; S3 w8 y- a
  4. std::vector<int> vec;
    , S  M/ \  Z3 `) s

  5. ' F! [' ?6 L, R, o
  6. void Consumer() {
    . p! g0 L2 R3 O; f$ \4 k+ R8 u$ E+ j
  7.   std::unique_lock<std::mutex> lock(mutex);
    * \7 m' S' U: c' C7 `. f
  8.   while (vec.empty()) { // 将if改为while2 Z4 ?+ V% D! z# l1 Q  h7 k/ x" W' v
  9.       cv.wait(lock);2 o, Y( b! S5 z1 Z2 J
  10.   }6 Z" C# F6 v/ v5 ~
  11.   std::cout << "consumer " << vec.size() << "\n";; f: l% ]4 g  a! Y% @& e' p7 O/ z
  12. }8 y# E7 u' Z) s& D2 ^2 y1 u; _5 m

  13. : h7 g, X! j' B; |0 G5 Z1 o3 M
  14. void Produce() {
    6 P+ ~  v# a" D- T, Y% c1 F% M4 n
  15.   std::unique_lock<std::mutex> lock(mutex);
    1 W3 m) x4 m1 s
  16.   vec.push_back(1);5 Q3 O2 D- t9 G* _
  17.   cv.notify_all();3 Z2 u' I8 m  _: n8 l
  18.   std::cout << "produce \n";
    5 w* E5 |: i& h& W# c
  19. }1 w9 q8 h% n$ S( M4 s' J

  20. 7 E& T1 _. B- a2 s
  21. int main() {0 I3 Q$ i' a: h; v& @8 {8 k5 J, |8 h
  22.   std::thread t(Consumer);
    ; S6 X: @4 I" s" J
  23.   t.detach();) [! G9 b* L9 J% _. @
  24.   Produce();& z- c  x* Y+ i  V4 Z2 v
  25.   return 0;  x) K( O* S8 |. X5 C
  26. }
复制代码
看到这里相信你已经明白条件变量的使用啦,需要使用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
  1. , a" V7 U, X! ?/ {: R. g1 g
  2. std::mutex mutex;1 P& w# n" c2 b4 `5 a
  3. std::condition_variable cv;
    # N2 F, k0 C; a5 X3 J0 o* p9 l9 H8 E
  4. std::vector<int> vec;
    $ e$ G- V' y" ?! }

  5. ( T6 N  c9 X' o& l! _) {
  6. void Consumer() {
    1 ]# B5 T+ p( \  P
  7.   std::unique_lock<std::mutex> lock(mutex);
    . \; C3 y4 B" B2 O
  8.   cv.wait(lock, [&](){ return !vec.empty(); }); // 这里可以直接使用C++的封装7 C2 ]+ N! G$ Q9 s
  9.   std::cout << "consumer " << vec.size() << "\n";+ @" Y4 W0 P/ ^; y" x, l2 T  }/ G
  10. }
    4 T9 i- C* m. t& v$ v
  11. / L. k; t5 G# c
  12. void Produce() {
    * }! F. m$ n" B+ J% A
  13.   std::unique_lock<std::mutex> lock(mutex);
    $ L0 Y# A6 _- r
  14.   vec.push_back(1);
    ! P4 b) r  q6 v9 }! w' K) c+ Z4 s
  15.   cv.notify_all();
    5 u4 }0 A- w  n+ D4 E9 {
  16.   std::cout << "produce \n";' [, z* v: H# H3 ^
  17. }
    ! p1 I" {1 a1 K! R. w, m2 _4 _
  18. 1 z+ O1 z% h- B, _/ u; q2 L
  19. int main() {
    9 g4 k3 I  {' K  ^3 b
  20.   std::thread t(Consumer);% ?: O; Q( S. d" t# ?3 L
  21.   t.detach();
    1 c  Z9 ?) j+ a) @- v& a* `3 b% N
  22.   Produce();
    , i# a/ G  d! p# E6 z0 [
  23.   return 0;8 e" C& [2 t) ?" ^& N- `
  24. }
复制代码
但在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
赞 收藏 评论0 发布时间:2020-8-28 21:53

举报

0个回答

所属标签

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版