创建任务
对于上一个练习,你的解决方案应该看起来像这样:
#![allow(unused)] fn main() { 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
是一个异步函数),从而允许执行器在此期间运行其他任务。
但是,我们如何实际并行运行多个任务呢?如果我们总是运行异步函数直到完成(通过使用.await
),那么任何时候都只会有一个任务在运行。
这就引出了tokio::spawn
函数的作用。
tokio::spawn
tokio::spawn
允许你将任务交给执行器处理,无需等待它完成。每次调用tokio::spawn
时,你实际上是告诉tokio
继续在后台运行这个被派生的任务,与派生它的任务并发进行。
以下是使用它并发处理多个连接的方式:
#![allow(unused)] fn main() { use tokio::net::TcpListener; pub async fn echo(listener: TcpListener) -> Result<(), anyhow::Error> { loop { let (mut socket, _) = listener.accept().await?; // 在后台启动一个任务以处理连接 // 从而使主任务能够立即开始接受新连接 tokio::spawn(async move { let (mut reader, mut writer) = socket.split(); tokio::io::copy(&mut reader, &mut writer).await?; }); } } }
异步块
在这个例子中,我们向tokio::spawn
传递了一个异步块:async move { /* */ }
异步块是一种快速标记代码区域为异步的方式,而不需要定义单独的异步函数。
JoinHandle
tokio::spawn
返回一个JoinHandle
。你可以使用JoinHandle
来.await
后台任务,就像我们对线程使用join
一样。
#![allow(unused)] fn main() { pub async fn run() { // 在后台启动一个任务以向远程服务器发送遥测数据 let handle = tokio::spawn(emit_telemetry()); // 同时做一些其他有用的工作 do_work().await; // 但在遥测数据成功发送之前不要返回给调用者 handle.await; } pub async fn emit_telemetry() { // [...] } pub async fn do_work() { // [...] } }
惊群边界
如果使用tokio::spawn
启动的任务发生了恐慌(panic),恐慌会被执行器捕获。如果你不.await
对应的JoinHandle
,恐慌就不会传播给启动者。即使你确实.await
了JoinHandle
,恐慌也不会自动传播。等待JoinHandle
会返回一个Result
,错误类型为JoinError
。然后你可以通过调用JoinError::is_panic
来检查任务是否发生了恐慌,并选择如何处理这个恐慌——比如记录它、忽略它或传播它。
#![allow(unused)] fn main() { 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::resume_unwind(reason); } } } pub async fn work() { // [...] } }
std::thread::spawn
与tokio::spawn
对比
你可以把tokio::spawn
想象成std::thread::spawn
的异步兄弟。
注意一个关键区别:使用std::thread::spawn
时,你将控制权委托给了操作系统调度器。你无法控制线程是如何调度的。
而在使用tokio::spawn
时,你将控制权委托给了完全在用户空间运行的异步执行器。底层的操作系统调度器并不参与决定下一个运行哪个任务。现在,通过我们选择使用的执行器,我们负责这个决策。