Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

select!

本集目標

學會用 select! 等待「多個 branch 中第一個完成的」,並理解它和取消(cancellation)的密切關係。

正文

等「誰先到」

join! 等的是「全部都完成」。select! 可說是它的一種相反:它同時等多個 branch,只要第一個完成,就執行那個 branch 對應的 handler,然後整個 select! 就結束——其他還沒完成的 branch 會被 drop

基本語法

select! 的每個 branch 大致長這樣:

tokio::select! {
    pattern = future => {
        // future 完成後,輸出會被 pattern 接住
    }
    _ = other_future => {
        // 不關心 other_future 的輸出
    }
}

等號前的 pattern 用來接住等號後那個 Future 的輸出;如果 pattern 裡綁定了變數,那個變數會被帶進右邊的大括號裡使用。等號後寫的是要等待的 Future 而已,這裡不要自己加 .awaitselect! 會負責同時 poll 這些 Future,等其中一個先完成。

如果你不需要某個 Future 的輸出,就像一般 match pattern 一樣用 _ 忽略它:

tokio::select! {
    value = compute() => {
        println!("算出來了:{}", value);
    }
    _ = shutdown.recv() => {
        println!("收到 shutdown 訊號");
    }
}

如果輸出本身是 Option<T>Result<T, E>,最直覺的寫法是先把整個值接住,再在 handler 裡自己 match

tokio::select! {
    message = receiver.recv() => {
        match message {
            Some(message) => println!("收到訊息:{}", message),
            None => println!("channel 關閉"),
        }
    }
    _ = shutdown.recv() => {
        println!("準備關閉");
    }
}

select! 本身也可以有回傳值,回傳的是勝出 branch 大括號裡最後一個 expression。這點跟 match 很像:每個 branch 都要回傳同一種型別。

let status = tokio::select! {
    value = compute() => {
        println!("算出來了:{}", value);
        "done"
    }
    _ = shutdown.recv() => {
        println!("收到 shutdown 訊號");
        "shutdown"
    }
};

println!("狀態:{}", status);

select! 最經典的用途是 timeout:把「真正的工作」和「一個計時器」一起 select!,看誰先到。

extern crate tokio;

use tokio::time::{sleep, Duration};

async fn do_work() {
    sleep(Duration::from_secs(5)).await; // 假設工作要五秒
    println!("工作完成");
}

#[tokio::main]
async fn main() {
    tokio::select! {
        _ = do_work() => {
            println!("工作順利做完了");
        }
        _ = sleep(Duration::from_secs(1)) => {
            println!("逾時!工作太久了,不等了");
        }
    }
}

這裡計時器一秒就到,比五秒的工作快,所以 select! 走計時器那個 branch、印出「逾時」,然後do_work() 那個 Future drop——工作就此被取消。

select! 很適合這些情境:

  • timeout(上面的例子)。
  • 同時接收多個 channel:哪個 channel 先有訊息就處理哪個。
  • 等待 shutdown signal:一邊做正常工作,一邊聽「該收工了」的訊號,誰先到就反應誰。

在迴圈裡用 select! 要小心 cancellation safety

剛剛提到了 drop:這正是上一集講的 cancellationdrop 一個 Future 就是取消它。而 select! 天生就會在某個 branch 勝出時,把其他 branch 全部 drop。理解這一點,後面用 select! 才不會踩雷。

select! 常常被放在 loop 裡反覆使用(例如一個伺服器迴圈:每輪 select! 等「新工作」或「shutdown 訊號」)。這種寫法要特別小心上一集的 cancellation safety

回想上一集:read_exact 這類「跨多次推進、累積中間狀態」的操作不是 cancellation safe,中途被取消時,可能已經消費了一部分資料,但整個「讀滿 buffer」的動作沒有完成。而 select! 每一輪都可能因為別的 branch 先完成,而把這一個 branch 的 Future drop 掉(取消)。如果你把 read_exact 放進 select! 的某個 branch,又在 loop 裡反覆跑,那它就很可能在讀到一半時被取消,留下不好接續的半成品。

輸掉的 branch 不會執行自己的大括號,這本來就是 select! 的正常行為,問題不在這裡。真正要小心的是:那個輸掉的 Future 在被丟掉以前,可能已經造成一部分外部效果,例如從 socket 讀走一些 bytes,或把一部分資料寫出去。

所以風險不是「handler 沒跑」,而是「Future 被取消時,已經做了一半的事沒有被完整收尾」。如果這個操作需要跨多步累積進度,就應該把進度放在 select! 外面,branch 裡只等待一次可以安全取消的小步驟。本章後面我們會示範如何遵守這樣的設計原則。

幾個實用補充

select! 還有幾個常用功能:

branch precondition:在 branch 後面加 , if 條件。條件為假時,這個 branch 直接略過,不會參與本輪競爭。

tokio::select! {
    Some(job) = jobs.recv(), if accepting_jobs => {
        handle(job).await;
    }
    _ = shutdown.recv() => {
        accepting_jobs = false;
    }
}

else 分支:當所有 branch 都被略過,沒有任何 branch 能跑時,執行 else

除了 if 失敗之外,如果右邊的 Future 完成了,但輸出不符合左邊的 pattern,這個 branch 也會在本輪 select! 被略過。例如 Some(job) = jobs.recv() 遇到 channel 關閉、recv()None 時,Some(job) 匹配失敗,這個 branch 就不會執行。如果所有 branch 都被略過,而且沒有 elseselect! 會 panic。

tokio::select! {
    Some(job) = jobs.recv(), if accepting_jobs => {
        handle(job).await;
    }
    Some(msg) = messages.recv(), if accepting_messages => {
        handle_message(msg).await;
    }
    else => {
        break; // 這一輪沒有任何 branch 能跑
    }
}

公平性與 biased;select! 預設是隨機挑選同時就緒的 branch(避免某個 branch 永遠被優先,餓死其他人)。如果你希望改成「由上到下依序檢查」,在開頭加一行 biased;

tokio::select! {
    biased; // 改成由上到下依序檢查,而非隨機
    _ = high_priority() => { /* ... */ }
    _ = low_priority()  => { /* ... */ }
}

重點整理

  • select! 同時等多個 branch,第一個完成就執行對應 handler,其他 branch 被 drop(取消)。
  • 基本語法是 pattern = future => { ... };branch 裡不用寫 .await,不需要輸出時用 _ = futureselect! 本身也能回傳勝出 branch 的值。
  • 所以 select! 是程式裡最常製造取消的地方;適合 timeout、多 channel 接收、等 shutdown 訊號。
  • loop 裡用 select! 要注意 cancellation safety:別把 read_exact 這類不可安全取消的 Future 放進會被 drop 的 branch。
  • 補充功能:branch if(precondition)、pattern 不匹配時略過該 branch、else(所有 branch 都被略過時)、biased; 把預設的隨機改成依序。