rust语言学习笔记——Tokio库(异步编程)

一、理解 Tokio

1. 核心概念:同步 vs 异步

  • 传统多线程(同步):就像餐厅里每个服务员(线程)只服务一桌客人。如果客人看菜单看了很久(IO 等待),服务员就干站着,浪费人力。
  • Tokio 异步模型‌:就像一个超级服务员同时照顾所有桌子。当客人看菜单时(IO 等待),服务员立刻去给另一桌倒水。只有当客人准备好点菜时(IO 就绪),服务员才回来处理。
  • 结论 ‌:Tokio 专为 ‌IO 密集型 ‌(网络请求、文件读写、数据库查询)设计。如果是纯 CPU 计算(如视频编码),请使用 rayon crate

2. 三大支柱

Tokio 运行时主要由三个部分组成:

  1. 反应器 (Reactor):监听 IO 事件(如 socket 可读、定时器到期)。底层依赖 OS 的多路复用机制(Linux epoll, macOS kqueue)。
  2. 执行器 (Executor):负责调度任务。它有一个工作窃取(Work-Stealing)调度器,确保多个 CPU 核心负载均衡。
  3. 任务 (Task):轻量级的"绿色线程"。创建成本极低,一个 OS 线程可以运行成千上万个 Task。

二、Tokio 基础

1. #[tokio::main]:应用的入口

rust 复制代码
use tokio::time::{sleep, Duration};

// 默认使用多线程运行时,线程数等于 CPU 核心数
#[tokio::main]
async fn main() {
    println!("启动!");
    // sleep 不会阻塞 OS 线程,而是让出控制权给其他任务
    sleep(Duration::from_secs(1)).await;
    println!("1秒后醒来");
}
  • 指定线程数:

    rust 复制代码
    #[tokio::main(flavor = "multi_thread", worker_threads = 4)]
    // 默认为多线程 flavor = "multi_thread",可以省略,建议显式声明
    #[tokio::main(worker_threads = 4)]
  • 使用单线程:

    rust 复制代码
    #[tokio::main(flavor = "current_thread")]

2. tokio::spawn:并发执行

spawn 用于在后台启动一个新任务。它返回一个 JoinHandle,你可以用它来等待结果或取消任务。

2.1 异步任务无返回值

rust 复制代码
use tokio::time::{Duration, sleep};

#[tokio::main]
async fn main() {
    // 启动一个异步任务,1秒后醒来
    let handle = tokio::spawn(async move {
        println!("启动!");
        sleep(Duration::from_secs(1)).await;
        println!("1秒后醒来");
    });

    // 等待异步任务完成后,执行后续代码
    handle.await.unwrap();

    println!("继续执行!");
}

2.2 异步任务有返回值

返回结果必须满足 Send + 'static,因为任务可能会在不同的 OS 线程间迁移。

rust 复制代码
use tokio::time::{Duration, sleep};

#[tokio::main]
async fn main() {
    // 启动一个异步任务,1秒后醒来
    let handle = tokio::spawn(async move {
        println!("启动!");
        sleep(Duration::from_secs(1)).await;
        println!("1秒后醒来");
        55
    });

    // 等待异步任务完成后,执行后续代码
    let result = handle.await.unwrap();

    println!("返回结果:{}", result);
}

三、Tokio 流程控制

1. tokio::join!:并行等待所有

同时发起多个请求,并等它们‌全部‌完成后一起处理结果时使用

rust 复制代码
async fn get_user() -> String {
    "张三".to_string()
}
async fn get_age() -> i32 {
    18
}

#[tokio::main]
async fn main() {
    // 两个任务同时开始执行
    let (user, age) = tokio::join!(get_user(), get_age());
    println!("用户名:{},年龄:{}", user, age);
}

2. tokio::select!:竞争等待第一个

当你关心‌谁先完成‌,或者需要设置超时时使用。未完成的分支会被自动取消(Drop)。

2.1 无返回值

rust 复制代码
use tokio::time::{Duration, sleep};

async fn get_user() -> String {
    sleep(Duration::from_secs(2)).await;
    "张三".to_string()
}

#[tokio::main]
async fn main() {
    tokio::select! {
        user=get_user()=>{
            println!("用户名:{}",user);
        },
        _ =sleep(Duration::from_secs(1))=>{
            println!("已超时!");
        },
    }
    println!("程序结束!");
}

2.2 有返回值

rust 复制代码
use tokio::time::{Duration, sleep};

async fn get_user() -> String {
    sleep(Duration::from_secs(2)).await;
    "张三".to_string()
}

#[tokio::main]
async fn main() {
    let username = tokio::select! {
        user=get_user()=>{
            println!("用户名:{}",user);
            user
        },
        _ =sleep(Duration::from_secs(1))=>{
            println!("已超时!");
            "无名".to_string()
        },
    };
    println!("程序结束!用户名:{}", username);
}

3. JoinSet:管理动态任务组

需要在循环中生成大量任务,并且希望有序地收集结果或统一取消,JoinSet 比手动维护 Vec<JoinHandle> 更安全、更优雅。

  • .join_all().join_next() 均为无序等待。

3.1 .join_all() 等待所有异步任务完成

rust 复制代码
use tokio::task::JoinSet;

#[tokio::main]
async fn main() {
    // 创建一个JoinSet, 用于管理多个异步任务
    let mut set = JoinSet::new();
    for i in 0..10 {
        // 启动一个异步任务, 并将结果加入到JoinSet中
        set.spawn(async move { i });
    }
    // 等待所有任务完成, 并返回所有任务的结果
    let results = set.join_all().await;
    // 遍历所有任务的结果, 并打印出来
    for result in results {
        println!("{}", result);
    }
}

3.2 .join_next() 逐个获取异步任务结果

rust 复制代码
use tokio::task::JoinSet;

#[tokio::main]
async fn main() {
    // 创建一个JoinSet, 用于管理多个异步任务
    let mut set = JoinSet::new();
    for i in 0..10 {
        // 启动一个异步任务, 并将结果加入到JoinSet中
        set.spawn(async move { i });
    }
    while let Some(res) = set.join_next().await {
        match res {
            Ok(res) => println!("{}", res),
            Err(e) => println!("error:{:?}", e),
        }
    }
}

四、共享状态与通信

在异步环境中,标准的 std::sync::Mutex 会阻塞整个线程。需要使用 Tokio 提供的异步版本。

1. tokio::sync::Mutex / RwLock 读写锁

用于保护共享数据。‌关键原则 ‌:尽量缩短持锁时间,不要在持有锁的时候执行 .await(除非你明确知道自己在做什么,否则容易死锁或降低并发度)。

1.1 tokio::sync::Mutex 读写均锁

  • 互斥访问 ‌:同一时刻只允许‌一个‌任务持有锁(无论是读还是写)。
  • 开销较低‌:内部状态简单(通常只是一个原子标志位 + 等待队列)。
  • 适用‌:临界区操作频繁、读写比例相当、或临界区非常短小的场景。
rust 复制代码
use std::sync::Arc;

use tokio::sync::Mutex;

#[tokio::main]
async fn main() {
    // 创建一个Arc<Mutex<T>>,用于在多个线程之间共享和修改数据
    let data = Arc::new(Mutex::new(0));
    // 创建10个线程,每个线程修改数据的值
    let mut handles = vec![];
    
    for _ in 0..10 {
        // 克隆数据,每个线程都有自己的数据副本
        let data_clone = Arc::clone(&data);
        
        // 每个线程修改数据的值
        handles.push(tokio::spawn(async move {
            // 每个线程获取数据的互斥锁,确保线程安全
            let mut num = data_clone.lock().await;
            // 每个线程修改数据的值
            *num += 1;
            
            // 每个线程释放互斥锁,允许其他线程访问数据
            // 不显式释放时,离开作用域自动释放
            drop(num);
        }));
    }

    // 顺序等待所有线程完成,串行执行
    for handle in handles {
        handle.await.unwrap();
    }

    println!("最终值:{}", *data.lock().await);
}

1.2 tokio::sync::RwLock 写锁,读不锁

  • 读写分离 ‌:允许‌多个 ‌读者同时持有读锁,或‌一个‌写者持有写锁。
  • 开销较高‌:需要维护更复杂的状态(读者计数、写者状态、两个等待队列等)。每次加锁/解锁涉及更多的原子操作和逻辑判断。
  • 适用 ‌:‌读多写少 ‌的场景(如缓存查询、配置读取)。如果写操作频繁,RwLock 的额外开销可能使其性能不如 Mutex
rust 复制代码
use std::sync::Arc;
use tokio::sync::RwLock;

#[tokio::main]
async fn main() {
    // 创建一个Arc<RwLock<T>>,用于在多个线程之间共享和修改数据
    let data = Arc::new(RwLock::new(vec![2, 3, 4, 5]));

    // 创建多个读取线程
    let r1 = tokio::spawn({
        let data1 = Arc::clone(&data);
        async move {
            let d = data1.read().await;
            println!("{:?}", d);
        }
    });
    let r2 = tokio::spawn({
        let data2 = Arc::clone(&data);
        async move {
            let d = data2.read().await;
            println!("{:?}", d);
        }
    });

    // 创建一个写入线程
    let w = tokio::spawn({
        let data3 = Arc::clone(&data);
        async move {
            let mut d = data3.write().await;
            d.push(55);
        }
    });

    // 等待所有线程完成
    r1.await.unwrap();
    r2.await.unwrap();
    w.await.unwrap();

    println!("最后结果:{:?}", data.read().await);
}

2. tokio::sync::mpsc:通道通信

推荐通过"消息传递"来共享状态,而不是共享内存。

rust 复制代码
use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    // 创建一个通道,缓存32个消息
    let (tx, mut rx) = mpsc::channel(32);

    // 启动一个异步任务,向通道发送消息
    tokio::spawn(async move {
        for i in 0..10 {
            tx.send(i).await.unwrap();
        }
    });

    // 从通道接收消息
    // 当通道关闭时,recv方法会返回None
    while let Some(d) = rx.recv().await {
        println!("收到消息:{}", d);
    }
}

3. tokio::sync::Semaphore:限流器

用于限制并发数量。

rust 复制代码
use std::sync::Arc;
use tokio::sync::Semaphore;

#[tokio::main]
async fn main() {
    // 最多3个任务同时执行
    let semaphore = Arc::new(Semaphore::new(3));
    let mut handles = vec![];

    for i in 0..10 {
        let sem = Arc::clone(&semaphore);
        handles.push(tokio::spawn(async move {
            
            // 如果满3个任务,会等待许可
            let _permit = sem.acquire().await.unwrap();
            
            println!("正在执行任务 {}。。。 ", i);
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        }));
    }

    // 等待所有任务完成
    for handle in handles {
        handle.await.unwrap();
    }
}

五、高级特性与性能优化

1、spawn_blocking:处理阻塞操作

非常重要 ‌。如果你在异步代码中调用了同步阻塞函数(如 std::fs::read_to_string 或复杂的 CPU 计算),它会卡住当前的 Worker 线程,导致整个运行时停滞。

解决方案 ‌:使用 spawn_blocking 将任务卸载到专门的阻塞线程池。

rust 复制代码
#[tokio::main]
async fn main() {
    let result = tokio::task::spawn_blocking(|| {
        // io 堵塞执行或CPU密集型任务,不能在async任务中执行,否则会导致阻塞
        println!("正在执行堵塞任务。。。 ");
        std::thread::sleep(std::time::Duration::from_secs(1));
        println!("堵塞任务执行完成");
        26
    });
    
    // 执行非堵塞任务
    for i in 0..10 {
        tokio::spawn(async move {
            println!("执行非堵塞任务 {}。。。 ", i);
        });
    }

    println!("堵塞任务执行结果: {}", result.await.unwrap());
}

2、task_local 异步任务局部存储

在异步调用链中安全地传递上下文数据,无需修改每个函数的参数。

它利用 task_local! 宏定义了一个绑定到当前异步任务生命周期的变量 REQUEST_ID,并在异步调用链中无需显式参数传递即可访问该数据。

rust 复制代码
use std::cell::RefCell;
use tokio::task_local;

// 1、定义一个任务局部变量
task_local! {
    static REQUEST_ID: RefCell<u64>;
}

#[tokio::main]
async fn main() {
    // 2、设置上下文,参数1为请求 ID,参数2为处理函数
    REQUEST_ID
        .scope(RefCell::new(12345), async {
            process_request().await;
        })
        .await;
}

async fn process_request() {
    // 3、在任何深层调用中都可以访问
    REQUEST_ID.with(|id| {
        println!("当前请求 ID: {}", id.borrow());
    });
}

3、运行时调优 (tokio::runtime::Builder)

在生产环境中,建议使用 Builder 手动构建运行时,以便精细控制资源。

rust 复制代码
fn main() {
    // 配置运行时:核心线程数=4,最大线程数=8
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(4)                    // 核心线程数
        .max_blocking_threads(8)              // 最大阻塞线程数
        .thread_stack_size(5 * 1024 * 1024)   // 线程栈大小
        .event_interval(20)   // 默认调度器时间间隔为61ms
        .enable_all()         // 启用所有IO、时间等特性
        .build()             
    // 构建运行时
        .unwrap();

    // 在运行时中执行异步任务
    rt.block_on(async {
        println!("手动构建的运行时执行异步任务");
    });
}

block_on 方法会阻塞当前线程,直到异步任务执行完成,仅用于运行时入口或同步代码调用异步逻辑的场景。

六、tokio生态

1、tokio::net:异步 TCP/UDP/UdsSocket

替代 std::net

2、tokio::fs:异步文件操作

注意:底层通常也是通过 spawn_blocking 实现的,所以对于极高并发文件 IO,可能需要更专业的库或直接使用阻塞线程池。

3、tokio::time‌:sleep, interval, timeout

4、tokio::signal:处理 Ctrl-C 等信号,实现优雅退出。

  • 特点 ‌:简单直接,适合单任务或不需要复杂并发控制的场景。但在主线程等待信号期间,如果需要同时运行其他业务逻辑,需要配合 tokio::spawn 使用。
rust 复制代码
use tokio::signal;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("服务启动,按 Ctrl+C 退出...");

    // 异步等待 Ctrl+C 信号,不会阻塞其他异步任务
    signal::ctrl_c().await?;

    println!("收到关闭信号,执行清理工作...");
    // 在此处添加资源释放、数据保存等逻辑

    Ok(())
}

4.1 使用原子标志位

rust 复制代码
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::signal;
use tokio::time::{Duration, sleep};

#[tokio::main]
async fn main() {
    // 创建共享的关闭标志,原子操作,初始值为 false,比Mutex更轻量级,更符合异步编程的场景
    let shutdown_flag = Arc::new(AtomicBool::new(false));
    // 克隆关闭标志,用于后台任务设置关闭标志
    let flag_clone = shutdown_flag.clone();

    // 启动后台任务监听信号
    tokio::spawn(async move {
        if let Err(e) = signal::ctrl_c().await {
            eprintln!("未能监听信号: {}", e);
        }
        println!("收到 Ctrl+C,设置关闭标志...");
        // 设置关闭标志为 true,通知主循环退出,Ordering::SeqCst 确保线程安全,所有线程都能看到最新的值
        flag_clone.store(true, Ordering::SeqCst);
    });

    // 模拟主业务循环
    loop {
        // 检查是否收到关闭信号,Ordering::SeqCst 确保线程安全,所有线程都能看到最新的值
        if shutdown_flag.load(Ordering::SeqCst) {
            println!("检测到关闭信号,退出主循环。");
            break;
        }

        println!("正在处理业务...");
        sleep(Duration::from_secs(1)).await;
    }

    println!("执行最终清理...");
}

4.2 使用 tokio::sync::watchbroadcast 通道

  • tokio::sync::watch::channel(<T>) 只存储一个值的通道,适合分发单一状态变更。
rust 复制代码
use tokio::signal;
use tokio::sync::watch;
use tokio::time::{Duration, sleep};

#[tokio::main]
async fn main() {
    // 创建 watch 通道,初始值为 false (未关闭)
    let (shutdown_tx, mut shutdown_rx) = watch::channel(false);

    // 启动一个模拟的工作任务
    let worker_handle = tokio::spawn(async move {
        loop {
            tokio::select! {
                // 等待数据变更后获取最新值,并执行清理操作
                _ = shutdown_rx.changed() => {
                    println!("工作任务收到关闭信号,开始清理...");
                    // 模拟异步清理操作,如写入磁盘
                    sleep(Duration::from_millis(500)).await;
                    println!("工作任务清理完毕,退出。");
                    break;
                }
                // 正常业务逻辑
                _ = sleep(Duration::from_secs(1)) => {
                    println!("工作任务正在运行...");
                }
            }
        }
    });

    println!("服务运行中,按 Ctrl+C 优雅退出...");
    // 主线程堵塞监听 Ctrl+C 信号
    signal::ctrl_c().await.expect("无法监听信号");

    println!("主线程收到信号,通知所有任务关闭...");
    // 发送关闭信号给所有任务
    let _ = shutdown_tx.send(true);

    // 等待工作任务完成
    let _ = worker_handle.await;

    println!("所有任务已停止,服务完全关闭。");
}

七、mpsc broadcastwatch 通道对比

特性 mpsc broadcast watch
全称 Multi-Producer, Single-Consumer Multi-Producer, Multi-Consumer Single-Producer, Multi-Consumer (状态)
生产者 多个 (Clone Sender) 多个 (Clone Sender) 单个 (通常不 Clone Sender)
消费者 仅 1 个 多个‌ (每个都接收全量消息) 多个‌ (每个都接收最新值)
消息保留 队列模式‌:保留所有未消费消息,先进先出 (FIFO) 缓冲模式‌:保留最近 N 条消息,新消费者从当前尾部开始接收 状态模式 ‌:‌只保留最新 1 个值‌,旧值立即被覆盖
背压机制 有界通道满时,发送者阻塞 缓冲区满时,发送者阻塞;慢消费者会收到 Lagged 错误 无背压,发送即完成
典型场景 任务队列、工作分发、日志聚合 事件广播、聊天室、发布/订阅系统 配置热更新、健康状态同步、全局开关

A. tokio::sync::mpsc:点对点的工作流

  • 核心逻辑‌:就像一条流水线,多个工人(生产者)把零件放上去,只有一个工人(消费者)在末端组装。
  • ‌关键行为:
    • 严格顺序‌:消息按发送顺序被单个消费者接收。
    • 独占消费 ‌:一旦消息被 recv() 取出,其他任务无法再看到该消息。
    • 背压保护 ‌:如果使用有界通道(channel(cap)),当消费者处理慢时,生产者会被阻塞,防止内存溢出。
  • 何时使用 ‌:当你需要确保‌每条数据都被且仅被处理一次‌时。例如:Web 服务器将请求放入队列,由后台 Worker 逐个处理。

B. tokio::sync::broadcast:一对多的事件广播

  • 核心逻辑‌:就像广播电台或微信群聊。一个人说话,所有在线的人都能听到。
  • 关键行为:
    • 全量复制 ‌:每个消费者(Receiver)都会收到‌每一条‌发送的消息。
    • 独立游标‌:每个消费者有自己的读取位置。如果消费者 A 处理慢,消费者 B 不受影响。
    • ‌**滞后处理 (Lagging)**‌:如果发送速度远超某个消费者的处理速度,导致缓冲区满了,该消费者再尝试接收时会得到 RecvError::Lagged,表示它丢失了一些中间消息。开发者需要决定是跳过这些消息还是重新同步。
    • 新加入者 ‌:新创建的 Receiver ‌不会‌收到之前发送的历史消息,只会收到从订阅时刻起的新消息。
  • 何时使用 ‌:当你需要‌通知多个组件同一件事‌,且允许少量消息丢失(或需自行处理丢失)时。例如:即时通讯软件中,一个用户发消息,所有在线用户都要收到;或者系统事件通知(如"数据库连接断开")。

C. tokio::sync::watch:最新状态同步

  • 核心逻辑 ‌:就像一块公共黑板。无论谁写,黑板上永远只显示‌最后写的那一行字‌。大家只关心当前写着什么,不关心之前写过什么。
  • ‌关键行为:
    • 只存最新‌:内部只存储一个值。发送新值时,旧值直接丢弃。
    • 变更通知 ‌:消费者通过 changed().await 等待值的变化。如果值没变,Future 不会完成。
    • 获取当前值 ‌:消费者可以随时通过 borrow() 获取当前最新的值引用,无需等待。
    • 无历史包袱 ‌:新订阅者可以通过 borrow() 立即拿到当前状态,而不需要像 broadcast 那样等待新消息。
  • 何时使用 ‌:当你只关心‌当前状态‌,而不关心中间过程时。例如:动态配置加载(Config Reload)、服务健康检查标志、功能开关(Feature Flag)。

八、tokio-console 异步程序调试工具

  • console-subscriber 会显著增加运行时开销并占用大量内存来存储历史事件。‌切勿在生产环境中启用此功能‌,仅用于开发和调试阶段。

1、配置 Cargo.toml 依赖

首先,需要在项目的 Cargo.toml 文件中添加 console-subscriber 依赖,并确保 tokio 启用了 tracing 特性。

toml 复制代码
[dependencies]
# 确保 tokio 版本兼容,并启用 full 或至少包含 rt-multi-thread 和 tracing特性
tokio = { version = "1", features = ["full", "tracing"] }
# 添加 console-subscriber
console-subscriber = "0.5" 

2、启用 Unstable 特性 (.cargo/config.toml)

项目目录下,手动创建文件夹和文件:.cargo/config.toml

toml 复制代码
[build]
rustflags = ["--cfg", "tokio_unstable"]

3、主程序中初始化subscriber

主程序man函数中开始位置添加:

rust 复制代码
console_subscriber::init();

4、项目目录下终端运行应用

bash 复制代码
cargo run

5、另一个终端运行

bash 复制代码
tokio-console
  • t 键切换到任务视图(Tasks)。

  • r 键切换到资源视图(Resources)。

  • 使用方向键选中某个任务,按 回车 键可以查看该任务的详细性能指标,如唤醒次数、忙/闲时间分布直方图等。

相关推荐
疯狂打码的少年1 小时前
【操作系统】板块总结 + 下期预告(软件工程)
笔记·软件工程
心中有国也有家2 小时前
AtomGit Flutter 鸿蒙客户端:一键记录情绪的极致体验
学习·flutter·华为·harmonyos
lauo2 小时前
从52起融资看教育科技新范式:PopLang引擎与Token节点经济如何重塑AI学习终端
人工智能·科技·学习
独孤留白2 小时前
从C到Rust:Trait Copy 数据位拷贝属性
rust
YUS云生2 小时前
Python学习笔记·第29天:Git进阶——分支操作与合并冲突
笔记·python·学习
xiaoyuchidayuma2 小时前
已知 ud、uq 求解三相相电压并计算调制比
笔记·学习
B8017913Y3 小时前
上课记笔记写不完不会整理?2026学习笔记生成使用场景该怎么选
人工智能·笔记·学习
小园子的小菜4 小时前
Java设计模式精讲:创建型+结构型模式(含生产级案例+相似模式对比)
学习·设计模式