Rust并发控制之Semaphore-两线程交替打印

信号量(Semaphore)是一种对资源并发访问控制的方式。

区别于互斥锁(Mutex)是对共享资源的独占访问,Semaphore允许指定多个并发访问共享资源。

就是说Semaphore像一个持有令牌(permit/token)的桶,每一个并发访问需要持有(acquire)一个令牌来访问共享资源,

当没有令牌时,没法访问共享资源,直到有新的令牌加入(add)或者原来发出的令牌放回(release)桶中。

接下来,我们尝试用通过用它来实现两个线程交替打印1和2,来更直观了解如何使用semaphore

Rust std库中没有正式发布的semaphore(std::sync::Semaphore在1.7.0废弃了)。下边用tokio库提供的semaphore

首先安装 tokio库

1
2
3
4
# 手动添加tokio到cargo.toml
# 或使用cargo-add: cargo add tokio --features sync,macros,rt-multi-thread
[dependencies]
tokio = { version = "1.34.0", features = ["sync", "macros", "rt-multi-thread"] }

先来一版常规实现,初始化一个只有一个令牌的semahore,两个线程去并发持有令牌,用后释放(通过drop)令牌,实现交替打印

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
use std::sync::Arc;
use tokio::sync::Semaphore;

#[tokio::main]
async fn main() {
let semaphore = Arc::new(Semaphore::new(1));
let cnt = 3;
let semaphore2 = semaphore.clone();

let t1 = tokio::spawn(async move {
for _ in 0..cnt {
let permit = semaphore.acquire().await.unwrap();
print!("1 ");
// 可不写,离开scope时自动释放,放回令牌桶
drop(permit);
}
});

let t2 = tokio::spawn(async move {
for _ in 0..cnt {
// 或用 _ ignore返回值,即时回收令牌
let _ = semaphore2.acquire().await.unwrap();
print!("2 ");
}
});

tokio::try_join!(t1, t2).unwrap();
}

乍看没什么问题,但是打印其实不一定是1 2 1 2 1 2的顺序。

原因很简单,我们只是约束了令牌同时只能有一个线程获取到,但是没有约束谁先谁后啊。所以其实没有实现交替打印。

怎么交替打印呢?

要控制顺序,我们可以让每个线程所持有的semaphore里的令牌时动态增加和消耗,然后一个令牌桶数量的增加滞后于另一个。

增加可以用add_permits, 消耗后不放回可以用forgot, 代码如下:

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
use std::sync::Arc;
use tokio::sync::Semaphore;

#[tokio::main]
async fn main() {
// 线程1的令牌桶1初始一个令牌,可以先打印1
let semaphore = Arc::new(Semaphore::new(1));
let cnt = 3;
let semaphore2 = semaphore.clone();

// 线程2的令牌桶2初始没有令牌,直到1打印后增加令牌
let semaphore_wait = Arc::new(Semaphore::new(0));
let semaphore_wait2 = semaphore_wait.clone();

let t1 = tokio::spawn(async move {
for _ in 0..cnt {
let permit = semaphore.acquire().await.unwrap();
print!("1 ");
// 消耗令牌,不放回令牌桶1
permit.forget();
// 令牌桶2增加令牌,可以打印2
semaphore_wait2.add_permits(1);
}
});

let t2 = tokio::spawn(async move {
for _ in 0..cnt {
let permit = semaphore_wait.acquire().await.unwrap();
print!("2 ");
// 消耗令牌,不放回令牌桶2
permit.forget();
// 令牌桶1增加令牌,可以打印1
semaphore2.add_permits(1);
}
});

tokio::try_join!(t1, t2).unwrap();
}

通过两个动态的令牌桶(semaphore)线程的执行顺序就能交替执行了。

可以和上篇 condvar实现的版本 对比下, 感受下semaphore的魅力。

如有疑问,请文末留言交流或邮件:newbvirgil@gmail.com 本文链接 : https://newbmiao.github.io/2023/11/24/rust-sync-semaphore.html