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

2023-11-27 12:33:41 浏览数 (3)

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

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

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

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

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

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

首先安装 tokio 库

代码语言:javascript复制
# 手动添加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)令牌,实现交替打印

代码语言:javascript复制
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, 代码如下:

代码语言:javascript复制
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 的魅力。


推荐阅读

  • 掌握Rust:从零开始的所有权之旅
  • Rust并发控制之Condvar-两线程交替打印
  • 聊聊共享所有权之Rc和Arc

如果有用,点个 在看,让更多人看到

外链不能跳转,戳 阅读原文 查看参考资料

0 人点赞