使用tokio::sync::Semaphore

信号量可以保证在某一时刻最多运行指定数量的并发任务。

使用信号量时,需在初始化时指定信号灯(tokio中的SemaphorePermit)的数量,每当任务要执行时,将从中取走一个信号灯,当任务完成时(信号灯被drop)会归还信号灯。当某个任务要执行时,如果此时信号灯数量为0,则该任务将等待,直到有信号灯被归还。因此,信号量通常用来提供类似于限量的功能。

例如,信号灯数量为1,表示所有并发任务必须串行运行,这种模式和互斥锁是类似的。再例如,信号灯数量设置为2,表示最多只有两个任务可以并发执行,如果有第三个任务,则必须等前两个任务中的某一个先完成。

例如:

use chrono::Local;
use std::sync::Arc;
use tokio::{ self, runtime::Runtime, sync::Semaphore, time::{self, Duration}};

fn now() -> String {
    Local::now().format("%F %T").to_string()
}

fn main() {
    let rt = Runtime::new().unwrap();
    rt.block_on(async {
        // 只有3个信号灯的信号量
        let semaphore = Arc::new(Semaphore::new(3));

        // 5个并发任务,每个任务执行前都先获取信号灯
        // 因此,同一时刻最多只有3个任务进行并发
        for i in 1..=5 {
            let semaphore = semaphore.clone();
            tokio::spawn(async move {
                let _permit = semaphore.acquire().await.unwrap();
                println!("{}, {}", i, now());
                time::sleep(Duration::from_secs(1)).await;
            });
        }

        time::sleep(Duration::from_secs(3)).await;
    });
}

互斥锁 Mutex实现

这是我在没有发现信号灯机制前用的方法,问的gpt,案例来自我自己的项目

    let tasks = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    for args in args_list {
        let app_handle = app.app_handle();
        let real_path_str_copy = real_path_str.clone();
        let tasks_ref = Arc::clone(&tasks);
        let handle = tokio::spawn(async move {
            let mut guard = tasks_ref.lock().await;
            while *guard >= max_tasks {
                drop(guard);
                task::yield_now().await;
                guard = tasks_ref.lock().await;
            }
            *guard += 1;
            drop(guard);

            run_task(app_handle, &args, &real_path_str_copy)
                .await
                .unwrap();
            let mut guard = tasks_ref.lock().await;
            *guard -= 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.await.unwrap();
    }

嗯,复杂多了,用互斥锁来获取当前运行了多少task,当 *guard >= max_tasks成立时,使用 task::tield_now().await;来把当前task的控制权移交给tokio而不往下执行。

怕while判断时,guard已经被丢弃,还在控制权移交的下一行增加了获取锁,rust不是很熟练,不是很清楚这里有没有起作用,因为理论上每一次 while循环 guard都离开了作用域,也就是会被自动 drop丢弃。

不管怎么说,It just work,并发确实被控制在了 max_tasks以内。

async move的语法创建一个异步block,会自动将所有用到的外部变量的所有权移交到block内,所以需要在定于block前复制一份。