- 示例工程
- 教程
【免费下载链接】100-exercises-to-learn-rust
A self-paced course to learn Rust, one exercise at a time.
本篇技术指南聚焦 100-exercises-to-learn-rust 课程中《08_futures》章节的Spawning tasks一节,围绕tokio::spawn讲解如何在异步 Rust 中让多个任务真正并发运行:从单任务的 echo 服务器出发,逐步引入async move异步块、JoinHandle、JoinError恐慌边界处理,并对比std::thread::spawn与tokio::spawn的调度差异。读完本文,你将掌握用tokio::spawn搭建并发服务器、等待后台任务完成、正确处理子任务 panic,以及理解Send + 'static约束背后原理的完整能力。
从单任务到多任务:为什么要spawn
回顾上一节练习(01_async_fn),一个基础 echo 服务器的解法大致如下:
pub async fn echo(listener: TcpListener) -> Result<(), anyhow::Error> { loop { let (mut socket, _) = listener.accept().await?; let (mut reader, mut writer) = socket.split(); tokio::io::copy(&mut reader, &mut writer).await?; } }这段代码本身并不差:如果两次连接到达之间隔了很长时间,echo函数会处于空闲状态——因为TcpListener::accept是异步函数,它会在等待时让出执行权,从而允许 executor 在此期间运行其他任务。
但问题在于:我们如何真正同时运行多个任务?如果始终用.await等待每个异步函数运行到完成,那么同一时刻永远只会有一个任务在执行。这正是tokio::spawn要解决的场景。
tokio::spawn:把任务交给 executor,不等它完成
tokio::spawn允许你将一个任务转交给 executor,而不必等待它完成。每次调用tokio::spawn,你都在告诉 tokio:让这个被 spawn 的任务在后台继续运行,与产生它的任务并发执行。
用它来处理多个并发连接非常简单:
use tokio::net::TcpListener; pub async fn echo(listener: TcpListener) -> Result<(), anyhow::Error> { loop { let (mut socket, _) = listener.accept().await?; // 在后台 spawn 一个任务处理该连接, // 从而让主任务可以立刻开始接受新连接 tokio::spawn(async move { let (mut reader, mut writer) = socket.split(); tokio::io::copy(&mut reader, &mut writer).await?; }); } }关键变化在于:accept 到连接后不再await完整的处理流程,而是把处理逻辑交给tokio::spawn,主循环立即返回去接受下一条连接。这样,每个连接都由一个独立的后台任务并发处理,服务器的吞吐能力得到本质提升。
异步块:async move { /* ... */ }
上面例子中传给tokio::spawn的是一个异步块(asynchronous block):async move { /* */ }。
异步块是在不单独定义异步函数的情况下,快速把一段代码标记为异步的语法手段。它同样返回一个 future,可以被 poll、被 spawn、被.await。move关键字则把外部变量(如socket)的所有权移入块内,满足 spawn 对'static生命周期的要求——这正是 03_runtime 一节中spawn签名约束的实践体现。
JoinHandle:等待后台任务完成
tokio::spawn会返回一个JoinHandle。你可以像对线程使用join一样,用JoinHandle来.await后台任务:
pub async fn run() { // 在后台 spawn 一个任务,把遥测数据发往远程服务器 let handle = tokio::spawn(emit_telemetry()); // 与此同时,做一些其他有用的事 do_work().await; // 但不要急着返回,直到遥测数据成功送达 handle.await; } pub async fn emit_telemetry() { // [...] } pub async fn do_work() { // [...] }这个模式在课程练习(03_runtime)中也有体现:tokio::spawn(fixed_reply(...))之后,测试用JoinSet同时发起多个客户端连接并逐一join_next(),验证服务器能够并发处理两个 listener 上的请求。
Panic 边界:子任务恐慌不会自动传播
如果一个由tokio::spawn产生的任务发生 panic,executor 会捕获这个 panic:
- 如果你没有
.await对应的JoinHandle,panic 不会传播给 spawn 方; - 即使你
.await了JoinHandle,panic也不会自动传播。
.await一个JoinHandle返回的是Result,错误类型为JoinError。你可以调用JoinError::is_panic判断任务是否 panic,然后自行决定如何处理——记录日志、忽略,或传播它:
use tokio::task::JoinError; pub async fn run() { let handle = tokio::spawn(work()); if let Err(e) = handle.await { if let Ok(reason) = e.try_into_panic() { // 任务发生了 panic // 我们恢复对该 panic 的展开(unwinding), // 从而将其传播给当前任务 panic::resume_unwind(reason); } } } pub async fn work() { // [...] }这正是课程练习(02_spawn 的测试)中使用的模式:while let Some(outcome) = join_set.join_next().await检查每个客户端任务的执行结果,若出错则通过e.try_into_panic()取出 panic 原因,再用panic::resume_unwind恢复展开,确保测试失败时能看到真实的恐慌信息。
std::thread::spawnvstokio::spawn
可以把tokio::spawn看作std::thread::spawn的异步孪生兄弟。但要留意一个关键区别:
- 使用
std::thread::spawn,你把控制权交给了OS 调度器,你无法决定线程如何被调度; - 使用
tokio::spawn,你委托给的是一个完全运行在用户空间的异步 executor,底层 OS 调度器不参与“下一个运行哪个任务”的决策——这个决策权现在属于你选择的 executor(在 tokio 中即任务调度与 work-stealing 机制,参见 Runtime architecture)。
这带来两大直接后果,也是tokio::spawn签名的约束来源:
pub fn spawn<F>(future: F) -> JoinHandle<F::Output> where F: Future + Send + 'static, F::Output: Send + 'static, { /* */ }'static约束:与std::thread::spawn同理,spawn 的任务可能活得比产生它的上下文更久,因此它不能依赖任何可能被提前释放的局部数据;Send约束:这是 work-stealing 策略的直接推论——在线程 A上 spawn 的任务可能被搬到空闲的线程 B上执行,跨越线程边界就必须Send。更深远地看,任何跨越.await点被持有的值都必须是Send的,否则编译器会以 “future cannot be sent between threads safely” 拒绝编译(详见 TheFuturetrait)。
用练习验证:并发 Echo 服务器
tokio::spawn的核心用法最终落在练习 02_spawn 上:实现一个echoes函数,同时接受两个TcpListener上的 TCP 连接,并且同一 listener 上的多个连接也要并发处理。测试通过tokio::spawn启动服务器,再借助JoinSet并发向两个地址发送多条消息,逐一校验回显内容。将单个tokio::spawn(async move { ... })扩展为处理两个 listener 的并发循环,即可通过全部测试——这也是对“spawn 一次 = 并发一路”这一模型最直接的实战验证。
运行该练习的命令(在仓库根目录下):
cargo test -p spawn依赖配置见 02_spawn/Cargo.toml:tokio = { version = "1", features = ["full"] }与anyhow = "1.0.100"。本练习的配套文档位于 book/src/08_futures/02_spawn.md,完整异步章节从 Async Rust 导读 开始,依次覆盖async fn、spawn、runtime 架构、Futuretrait 与“不要阻塞 runtime”等主题,可循序渐进系统学习。
- 示例工程
- 教程
【免费下载链接】100-exercises-to-learn-rust
A self-paced course to learn Rust, one exercise at a time.
相关推荐
如何用 Tokio 搭建 TCP echo 服务器并用 tokio::spawn 并发处理连接
如何用 Tokio 搭建 TCP echo 服务器并用 tokio::spawn 并发处理连接 你要完成的任务是:用 Tokio 搭一个 TCP echo 服务
后端异步编程通信并发编程Semantica evals模块详解:如何科学评估知识图谱构建质量
Semantica evals模块详解:如何科学评估知识图谱构建质量 构建知识图谱最头疼的不是"建出来",而是"怎么证明它建得对"。Semantica 开源项目
示例工程教程深入 Rust 并发:用 Mutex、Send 与 Arc 为 100-exercises-to-learn-rust 票务系统实现安全加锁
深入 Rust 并发:用 Mutex、Send 与 Arc 为 100 exercises to learn rust 票务系统实现安全加锁 导读 :在《100
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考