创建任务

对于上一个练习,你的解决方案应该看起来像这样:

#![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,恐慌就不会传播给启动者。即使你确实.awaitJoinHandle,恐慌也不会自动传播。等待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::spawntokio::spawn对比

你可以把tokio::spawn想象成std::thread::spawn的异步兄弟。

注意一个关键区别:使用std::thread::spawn时,你将控制权委托给了操作系统调度器。你无法控制线程是如何调度的。

而在使用tokio::spawn时,你将控制权委托给了完全在用户空间运行的异步执行器。底层的操作系统调度器并不参与决定下一个运行哪个任务。现在,通过我们选择使用的执行器,我们负责这个决策。