81、Spawning tasks(生成任务)
Spawning tasks
上一个练习的解答应该类似于这样:
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 在后台继续运行被生成的任务,并与生成它的原始任务并行运行。
以下是如何使用它来并发处理多个连接的例子:
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?;
});
}
}
Asynchronous blocks(异步块)
在这个例子中,我们向 tokio::spawn 传递了一个异步块:async move { /* */ } 异步块是一种将代码区域标记为异步的快速方法,而无需定义单独的异步函数。
JoinHandle
tokio::spawn 返回一个 JoinHandle 对象。
我们可以使用 JoinHandle 来 .await 后台任务的完成,就像我们使用 join 来等待新创建的线程一样。
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() {
// [...]
}
Panic boundary
如果使用 tokio::spawn 生成的任务发生 panic,执行器会捕获到该 panic。
如果我们没有 .await 相应的 JoinHandle,则 panic 不会传播到生成器(spawner)。即使我们使用了 .await 等待 JoinHandle,panic 也不会自动传播。等待 JoinHandle 会返回一个 Result,其错误类型为 JoinError。然后,您可以通过调用 JoinError::is_panic 来检查任务是否发生 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
// 我们resume unwind panic,
// 因此, panic 会传播到当前任务。
panic::resume_unwind(reason);
}
}
}
pub async fn work() {
// [...]
}
std::thread::spawn vs tokio::spawn
我们可以把 tokio::spawn 看作是 std::thread::spawn 的异步版本。
注意一个关键区别:使用 std::thread::spawn 时,我们是将控制权委托给了操作系统调度器。我们无法控制线程的调度方式。
而使用 tokio::spawn 时,我们是将控制权委托给了一个完全在用户空间运行的异步执行器。底层操作系统调度器不再参与决定下一个要运行的任务。现在,我们通过选择使用的执行器来掌控这个决定。
