0%

多线程原语ConVar

多线程原语CondVar

多线程下的原语,除了我们常用的锁,还有另外一类用于同步的原语叫做“屏障”,“条件变量”(在rust或者cpp中)。在其他语言中也有类似的概念,叫做栅栏,闭锁,屏障,信号量等。他们具有相同的意义。

在介绍条件变量之前,先介绍屏障(Barrier)。屏障相当于一堵带门的墙,使用wait方法,在某个点阻塞全部进入临界区的线程。条件变量(Condition Variable)和屏障的语义类似,但它不是阻塞全部线程,而是在满足某些特定条件之前阻塞某一个得到互斥锁的线程。

单纯讲条件变量的意义并不直观。换种描述

条件变量可以在我们达到某种条件之前阻塞线程,我们利用此特性可以对线程进行同步。或者说做到按照某种条件,在多个线程中达到按照特定顺序执行的目的。

为此我们设计如下下面流程。为此流程写一段代码,来体会条件变量的作用

我们启动三个线程,t1,t2,t3。分别执行任务T1,T2,T3。现在要求:T2必须等待T1和T3完成之后再执行

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::thread::sleep;
use std::time::Duration;


pub fn main() {
let pair = Arc::new((Mutex::new(0),
Condvar::new()));
let pair2 = pair.clone();
let pair3 = pair.clone();


let t1 = thread::Builder::new()
.name("T1".to_string())
.spawn(move ||
{
sleep(Duration::from_secs(4));
println!("I'm working in T1, step 1");
let &(ref lock, ref cvar) = &*pair2;
let mut started = lock.lock();
*started += 2;
cvar.notify_one();
}
)
.unwrap();

let t2 = thread::Builder::new()
.name("T2".to_string())
.spawn(move ||
{
println!("I'm working in T2, start");
let &(ref lock, ref cvar) = &*pair;
let mut notify = lock.lock();

while *notify < 5 {
cvar.wait(&mut notify);
}
println!("I'm working in T2, final");
}
)
.unwrap();

let t3 = thread::Builder::new()
.name("T3".to_string())
.spawn(move ||
{
sleep(Duration::from_secs(3));
println!("I'm working in T3, step 2");
let &(ref lock, ref cvar) = &*pair3;
let mut started = lock.lock();
*started += 3;
cvar.notify_one();
}
)
.unwrap();

t1.join().unwrap();
t2.join().unwrap();
t3.join().unwrap();
}

以上代码可以在 这个链接 下在playground运行。

上面的代码需要注意的点如下

  1. CondVar需要和锁一起使用,在运行中每个条件变量每次只可以和一个互斥体一起使用。
  2. 这里使用了parking_lot中的CondVar和Mutex,使用标准库中的条件变量和锁也是一样的效果。
  3. 设计中,在锁中持有了一个数字类型。当锁中的数字(也就是我们的“变量”)小于5,我们使用wait阻塞住t2。我们在t1完成时,把数字加二,在t3完成后,我们把数字加三。
  4. 注意,每次更改变量后要使用通知。
  5. 一般情况下,我们可以设计更复杂的变量和阻塞条件来达到更复杂的同步效果

特别注意的是

notify_one()只会通知线程一次,也就是说,如果我们有多个线程被阻塞住,notify_one会被一个阻塞地方消耗。不会传播到另一个阻塞的临界区中

notify_all()会通知所有阻塞区。

所以使用的时候需要特别注意两种通知不同的使用场景,避免造成阻塞。

在CondVar中还有对应的wait_for。可以设置TimeOut,避免造成永久的阻塞。