【譯】Async/Await(五)—— Executors and Wakers

Praying發表於2021-02-08

原文標題:Async/Await
原文連結:https://os.phil-opp.com/async-await/#multitasking
公眾號: Rust 碎碎念
翻譯 by: Praying

Executors and Wakers

使用 async/await,可以讓我們以一種全完非同步的方式來與 future 進行更為自然地協作。然而,正如我們之前所瞭解到的,future 在被輪詢之前什麼事也不會做。這意味著我們必須在某個時間點上呼叫poll,否則非同步的程式碼永遠都不會執行。

對於單個的 future,我們總是通過使用一個迴圈手動地等待每個 future。但這種方式十分地低效,且對於一個建立大量 future 的程式來講也不適用。針對這個問題的最常見的解決方式是定義一個全域性的executor

Executors(執行器)

executor 的作用在於能夠產生 future 作為獨立的任務,通常是通過某種spawn方法。接著 executor 負責輪詢所有的 future 直到它們完成。集中管理所有的 future 的巨大優勢在於,只要當一個 future 返回Poll::Pending時,executor 就可以切換到另一個 future。因此,非同步操作可以並行執行並且 CPU 始終儲存繁忙。

許多 executor 的實現充分利用 CPU 多核心的優勢,它們建立了一個執行緒池[1],該執行緒池能夠在工作足夠多的情況下充分利用所有的核心,並且使用類似work stealing[2]的方式在核心之間進行負載均衡。還有針對嵌入式系統優化了低延遲和記憶體負載的特殊的 executor 實現。

為了避免重複輪詢 future 的負擔,executor 通常會充分利用由 Rust 的 future 支援的 waker API。

Waker

Waker API 背後的設計理念是,一個特定的Waker[3]型別,包裝在Context型別中,被傳遞到poll的每一次執行。這個Waker型別由 executor 建立,並且可以被非同步任務用來通知自己的完成。因此,executor 不需要在一個 future 返回Poll::Pending之前對其呼叫poll,直到它被對應的 waker 呼叫。

這可以通過一個小例子來闡述:

async fn write_file() {
    async_write_file("foo.txt""Hello").await;
}

這個函式非同步地把一個字串“Hello”寫入到檔案foo.txt中。因為硬碟寫入需要一點兒時間,所以 future 上的第一次poll呼叫很大可能返回Poll::Pending。儘管如此,硬碟驅動將把傳遞給poll呼叫的Waker儲存起來,並在當檔案被完全寫入磁碟後使用它來提醒 executor,通過這種方式,executor 在收到 waker 提醒之前不需要浪費時間一次又一次地去輪詢這個 future。

當我們在後面的章節實現自己的支援 waker 的 executor 時,我們就會看到Waker型別更詳細的工作原理。

協作式多工?

在本文(系列)開頭,我們討論了搶佔式和協作式多工。搶佔式多工依賴於作業系統在執行中的任務間進行強制切換,協作式多工則需要任務通過一個yield操作自願放棄對 CPU 的控制權。協作式多工的巨大優勢在於,任務可以自己儲存自身狀態,從而產生更為高效的上下文切換,並使得在任務間共享相同的呼叫棧成為可能。

雖然看上去可能不太明顯,但是 future 和 async/await 是一種協作式多工模式的實現:

  • 每個被新增到 executor 的 future 是一個協作式任務。

  • 不同於顯式的yield操作,future 通過返回Poll::Pending(或者是結束時的Poll::Ready)來放棄對 CPU 核心的控制權。

    • 沒有什麼可以強制讓 future 放棄 CPU,future 可以永遠不從poll裡面返回,例如,無限迴圈。

    • 因為每個 future 都能阻塞 executor 中其他 future 的執行,所以我們需要確信它們不是惡意的。

  • Futures 內部儲存了需要在下次poll呼叫繼續執行所需的所有狀態。通過 async/await,編譯器會自動探測所有需要的變數並將其儲存在生成的狀態機內部。

    • 只儲存繼續執行需要的最小狀態
    • 因為poll方法在返回時放棄了呼叫棧,所以同一個棧可以被用於輪詢其他的 future。

我們可以看到,future 和 async/await 完美契合協作式多工模式,它們只是用了一些不同的技術。接下來,我們將會交替使用“任務(task)”和“future”。

參考資料

[1]

執行緒池: https://en.wikipedia.org/wiki/Thread_pool

[2]

work stealing: https://en.wikipedia.org/wiki/Work_stealing

[3]

Waker: https://doc.rust-lang.org/nightly/core/task/struct.Waker.html

```

相關文章