☰
100-exercises-to-learn-rust:用 `tokio::spawn` 实现真正的并发——从 Echo 服务器到任务并发控制
2026/10/3 8:19:13 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】100-exercises-to-learn-rust

A self-paced course to learn Rust, one exercise at a time.

项目地址:https://gitcode.com/GitHub_Trending/10/100-exercises-to-learn-rust
点击查看免费下载

本篇技术指南聚焦 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.

项目地址:https://gitcode.com/GitHub_Trending/10/100-exercises-to-learn-rust
点击查看免费下载
上一篇:python-machine-learning-book进阶:循环神经网络基础
下一篇:codesandbox-client中的量子人工智能开发:前沿科技探索

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询