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 时,我们是将控制权委托给了一个完全在用户空间运行的异步执行器。底层操作系统调度器不再参与决定下一个要运行的任务。现在,我们通过选择使用的执行器来掌控这个决定。

阅读剩余
THE END