优雅关机

此页面的目的是概述如何在异步应用程序中正确实现关机。

实现优雅关机通常有三个部分

  • 确定何时关机。
  • 告知程序的每个部分关机。
  • 等待程序的其他部分关机。

本文的其余部分将详细介绍这些部分。 此处描述的方法的真实世界实现可以在 mini-redis 中找到,特别是 src/server.rssrc/shutdown.rs 文件。

确定何时关机

这当然取决于应用程序,但一个非常常见的关机标准是应用程序收到来自操作系统的信号时。 例如,当程序运行时,您在终端中按下 ctrl+c 时,就会发生这种情况。 为了检测到这一点,Tokio 提供了一个 tokio::signal::ctrl_c 函数,该函数将休眠直到收到这样的信号。 您可以像这样使用它

use tokio::signal;

#[tokio::main]
async fn main() {
    // ... spawn application as separate task ...

    match signal::ctrl_c().await {
        Ok(()) => {},
        Err(err) => {
            eprintln!("Unable to listen for shutdown signal: {}", err);
            // we also shut down in case of error
        },
    }

    // send shutdown signal to application and wait
}

如果您有多个关机条件,您可以使用 mpsc 通道 将关机信号发送到一个地方。 然后,您可以 select ctrl_c 和通道。 例如

use tokio::signal;
use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (shutdown_send, mut shutdown_recv) = mpsc::unbounded_channel();

    // ... spawn application as separate task ...
    //
    // application uses shutdown_send in case a shutdown was issued from inside
    // the application

    tokio::select! {
        _ = signal::ctrl_c() => {},
        _ = shutdown_recv.recv() => {},
    }

    // send shutdown signal to application and wait
}

告知事物关机

当您想告诉一个或多个任务关机时,您可以使用 取消令牌。 这些令牌允许您通知任务,它们应该响应取消请求而终止自身,从而轻松实现优雅关机。

要在多个任务之间共享 CancellationToken,您必须克隆它。 这是由于单一所有权规则,该规则要求每个值都只有一个所有者。 克隆令牌时,您会获得另一个与原始令牌无法区分的令牌; 如果一个被取消,则另一个也会被取消。 您可以根据需要创建任意数量的克隆,当您在一个克隆上调用 cancel 时,它们都会被取消。

以下是在多个任务中使用 CancellationToken 的步骤

  1. 首先,创建一个新的 CancellationToken
  2. 然后,通过调用原始令牌上的 clone 方法来创建原始 CancellationToken 的克隆。 这将创建一个新令牌,可供另一个任务使用。
  3. 将原始令牌或克隆令牌传递给应响应取消请求的任务。
  4. 当您想优雅地关闭任务时,请调用原始令牌或克隆令牌上的 cancel 方法。 任何监听原始令牌或克隆令牌上的取消请求的任务都将收到关闭通知。

这是一个展示上述步骤的代码片段

// Step 1: Create a new CancellationToken
let token = CancellationToken::new();

// Step 2: Clone the token for use in another task
let cloned_token = token.clone();

// Task 1 - Wait for token cancellation or a long time
let task1_handle = tokio::spawn(async move {
    tokio::select! {
        // Step 3: Using cloned token to listen to cancellation requests
        _ = cloned_token.cancelled() => {
            // The token was cancelled, task can shut down
        }
        _ = tokio::time::sleep(std::time::Duration::from_secs(9999)) => {
            // Long work has completed
        }
    }
});

// Task 2 - Cancel the original token after a small delay
tokio::spawn(async move {
    tokio::time::sleep(std::time::Duration::from_millis(10)).await;

    // Step 4: Cancel the original or cloned token to notify other tasks about shutting down gracefully
    token.cancel();
});

// Wait for tasks to complete
task1_handle.await.unwrap()

使用取消令牌,您不必在令牌取消时立即关闭任务。 相反,您可以在终止任务之前运行关机程序,例如将数据刷新到文件或数据库,或在连接上发送关机消息。

等待事物完成关机

一旦您告诉其他任务关机,您将需要等待它们完成。 一种简单的方法是使用 任务跟踪器。 任务跟踪器是任务的集合。 任务跟踪器的 wait 方法为您提供了一个 future,该 future 仅在其包含的所有 future 都已解决 **并且** 任务跟踪器已关闭后才解析。

以下示例将衍生 10 个任务,然后使用任务跟踪器等待它们关机。

use std::time::Duration;
use tokio::time::sleep;
use tokio_util::task::TaskTracker;

#[tokio::main]
async fn main() {
    let tracker = TaskTracker::new();

    for i in 0..10 {
        tracker.spawn(some_operation(i));
    }

    // Once we spawned everything, we close the tracker.
    tracker.close();

    // Wait for everything to finish.
    tracker.wait().await;

    println!("This is printed after all of the tasks.");
}

async fn some_operation(i: u64) {
    sleep(Duration::from_millis(100 * i)).await;
    println!("Task {} shutting down.", i);
}