一、理解 Tokio
1. 核心概念:同步 vs 异步
- 传统多线程(同步):就像餐厅里每个服务员(线程)只服务一桌客人。如果客人看菜单看了很久(IO 等待),服务员就干站着,浪费人力。
- Tokio 异步模型:就像一个超级服务员同时照顾所有桌子。当客人看菜单时(IO 等待),服务员立刻去给另一桌倒水。只有当客人准备好点菜时(IO 就绪),服务员才回来处理。
- 结论 :Tokio 专为 IO 密集型 (网络请求、文件读写、数据库查询)设计。如果是纯 CPU 计算(如视频编码),请使用
rayon crate。
2. 三大支柱
Tokio 运行时主要由三个部分组成:
- 反应器 (Reactor):监听 IO 事件(如 socket 可读、定时器到期)。底层依赖 OS 的多路复用机制(Linux epoll, macOS kqueue)。
- 执行器 (Executor):负责调度任务。它有一个工作窃取(Work-Stealing)调度器,确保多个 CPU 核心负载均衡。
- 任务 (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::watch 或 broadcast 通道
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、 broadcast、watch 通道对比
| 特性 | 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)。
-
使用方向键选中某个任务,按
回车键可以查看该任务的详细性能指标,如唤醒次数、忙/闲时间分布直方图等。