rust-tokio-semaphore-concurrency

test12026-10-2619 次阅读

为什么需要信号量

tokio 下用 join! 或 spawn 一堆任务很轻松,但一次性并发几百个网络请求会把下游打爆,本地文件句柄和内存也扛不住。信号量就是一把限流闸:预先放 N 个许可,谁拿到许可谁才能开工,干完归还,并发数就被钉在 N 以内。

基础用法

Semaphore::new(3) 表示最多 3 个任务同时执行。用 acquire_owned 拿到一个可移动的许可,方便带进 async 任务。

use tokio::sync::Semaphore;
use std::sync::Arc;

async fn do_work(i: u32) {
    // 模拟一次耗时请求
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    println!("done {i}");
}

#[tokio::main]
async fn main() {
    let sem = Arc::new(Semaphore::new(3));

    let mut handles = vec![];
    for i in 0..10 {
        let permit = sem.clone().acquire_owned().await.unwrap();
        handles.push(tokio::spawn(async move {
            do_work(i).await;
            drop(permit); // 显式归还,离开作用域也会自动归还
        }));
    }

    for h in handles {
        let _ = h.await;
    }
}

在并发抓取里限流

把信号量带进抓取循环,每个 URL 先拿许可再 spawn,即使有上千个链接,同时在飞的网络请求也始终不超过 limit。

use tokio::sync::Semaphore;
use std::sync::Arc;

async fn fetch(url: String) -> Result<String, reqwest::Error> {
    Ok(reqwest::get(&url).await?.text().await?)
}

async fn fetch_all(urls: Vec<String>, limit: usize) -> Vec<Result<String, reqwest::Error>> {
    let sem = Arc::new(Semaphore::new(limit));
    let mut tasks = Vec::with_capacity(urls.len());

    for url in urls {
        let permit = sem.clone().acquire_owned().await.unwrap();
        tasks.push(tokio::spawn(async move {
            let result = fetch(url).await;
            drop(permit);
            result
        }));
    }

    let mut out = Vec::with_capacity(tasks.len());
    for t in tasks {
        out.push(t.await.unwrap());
    }
    out
}

注意点

  • 普通 acquire 借用的许可不能跨 await 带进 spawn 的任务,必须用 acquire_owned 拿一个可拥有的许可。
  • 许可靠 drop 或离开作用域释放,忘记归还就会永久少一个配额,并发上限慢慢被吃掉。
  • limit 大小看下游承受能力,爬虫一般 5 到 20,压测接口可以更小。
T

test1

文章作者

为什么需要信号量 tokio 下用 join! 或 spawn 一堆任务很轻松,但一次性并发几百个网络请求会把下游打爆...

分类
技术
发布时间
2026-10-26
字数
约 1763 字
阅读
19 次