【譯】理解Rust中的Futures (一)

Praying發表於2020-12-15

原文標題:Understanding Futures In Rust -- Part 1
原文連結:https://www.viget.com/articles/understanding-futures-in-rust-part-1/
公眾號: Rust 碎碎念
翻譯 by: Praying

背景

Rust 中的 Futures 類似於 Javascript 中的promise[1],它們是對 Rust 中併發原語的強大抽象。這也是通往async/await[2]的基石,async/await 能夠讓使用者像寫同步程式碼一樣來寫非同步程式碼。

Async/await 在 Rust 初期還沒有準備好,但是這並不意味著你不應該在你的 Rust 專案中開始使用 futures。tokio[3] crate 穩定、易用且快速。請檢視此文件[4]來了解使用 future 的入門知識。

Futures 已經在標準庫當中了,但是在這個系列的部落格中,我打算寫一個簡化版本來展示它是如何工作的、如何使用它以及避免一些常見的陷阱。

Tokio 的主分支正在使用 std::future,但是所有的文件都引用自 0.1 版本的 futures。不過,這些概念都是適用的。

儘管 futures 現在在 std 當中,但是缺失了很多常用的特性。這些特性當前在 future-preview[5] 中維護,並且我將會引用定義在其中的函式和 trait。事情進展得很快,那個 crate 裡的很多東西最終都會進入標準庫。

預備知識

  • 瞭解一些 Rust 的知識或者在接下來的過程中願意去學習 Rust(能夠閱讀Rust book[6]就更好了。)

  • 一個現代的瀏覽器,比如 Chrome,FireFox,Safari,或者 Edge(我們將會使用 rust playground[7]

  • 就這些!

目標

本文的目標是能夠理解下面的程式碼,並且實現所需的型別和函式來使其能夠編譯。這段程式碼對於標準庫的 futures 是有效的語法,並且說明鏈式 futures 是如何工作的。

// 這段程式碼目前還不能編譯

fn main() {
    let future1 = future::ok::<u32u32>(1)
        .map(|x| x + 3)
        .map_err(|e| println!("Error: {:?}", e))
        .and_then(|x| Ok(x - 3))
        .then(|res| {
          match res {
              Ok(val) => Ok(val + 3),
              err => err,
          }
        });
    let joined_future = future::join(future1, future::err::<u32u32>(2));
    let val = block_on(joined_future);
    assert_eq!(val, (Ok(4), Err(2)));
}

Future 到底是什麼?

具體來講,它是一系列非同步計算所代表的值。Futures crate 的文件稱其為“表示一個物件,該物件是另一個尚未準備好的值的代理(a concept for an object which is a proxy for another value that may not be ready yet)”。

Rust 中的 futures 允許你定義一個可以被非同步執行的任務,比如一個網路呼叫或者計算。你可以在那個結果上鍊接函式,對其進行轉換,處理錯誤,與其他的 futures 合併以及執行許多其他的計算。這些函式只有當 future 被傳遞給一個 executor,比如 tokio 的run函式,才會執行。事實上,如果你在離開作用域之前沒有使用 future,什麼事都不會發生。也因此,futures crate 宣告 futures 是must_use的,並且如果你允許它們沒有被使用就離開作用域,編譯器會給出一個警告。

如果你熟悉 JavaScript 的 promises,有些東西可能會覺得奇怪。在 JavaScript 中,promises 是在事件迴圈中被執行,並且沒有其他的可以執行它們的選擇。executor函式是立即執行的。但是,從本質上來講,promise 仍然只是簡單地定義了一系列將來要執行的指令。在 Rust 中,executor 可以選擇許多非同步策略中的任意一個來執行。

構建我們的 Future

從高一點的層次來講,我們需要一些程式碼片段來讓 futures 工作;一個 runner,future trait 以及 poll 型別。

首先,一個 Runner

如果我們沒有一種方式來執行我們的 future,它將不會做什麼事情。因為,我們正在實現我們自己的 futures,所以我們也需要實現我們自己的 runner。在這個練習中,我們實際上不會做任何非同步的事情,但是我們將會進行近似的非同步呼叫。
Futures 基於 pull 而不是基於 push。這使得 futures 能夠成為一個零抽象,但是這也意味著它們會被輪詢一次,並且在當它們準備能夠再次輪詢的時候負責提醒 executor。它工作方式的具體細節對於理解 futures 是如何被建立和連結到一起並不重要,因此,我們的 executor 只是一個非常粗略的近似。它只能執行一個 future,並且它不能做任何有意義的非同步。Tokio 文件有很多關於 futures 執行時模型的資訊。

下面是一個看起來非常簡單的實現:

use std::cell::RefCell;

thread_local!(static NOTIFY: RefCell<bool> = RefCell::new(true));

struct Context<'a> {
    waker: &'a Waker,
}

impl<'a> Context<'a> {
    fn from_waker(waker: &'a Waker) -> Self {
        Context { waker }
    }

    fn waker(&self) -> &'a Waker {
        &self.waker
    }
}

struct Waker;

impl Waker {
    fn wake(&self) {
        NOTIFY.with(|f| *f.borrow_mut() = true)
    }
}

fn run<F>(mut f: F) -> F::Output
where
    F: Future,
{
    NOTIFY.with(|n| loop {
        if *n.borrow() {
            *n.borrow_mut() = false;
            let ctx = Context::from_waker(&Waker);
            if let Poll::Ready(val) = f.poll(&ctx) {
                return val;
            }
        }
    })
}

run是一個泛型函式,其中 F 是一個 future,並且它返回一個定義在Future trait 中的Output型別的值,我們在後面會講到它。

函式體的邏輯近似於一個真實的 runner 可能會做的事情,它會一直迴圈直到被提醒 future 準備好被再次輪詢了。它會在 future 就緒時從函式返回。ContextWaker型別是對定義在future::task模組中的同名型別的模擬,可以在這裡[8]看到。編譯需要這裡有它們的存在,但是這不再本文的討論範圍之內。具體它們是怎麼實現的,你可以去自由探索。

Poll 是一個簡單的泛型列舉,我們可以像下面這樣定義它:

enum Poll<T> {
    Ready(T),
    Pending
}

我們的 Trait

Trait[9]是在 Rust 中定義共享行為的一種方式。它允許我們能夠指定實現型別必須定義的型別和函式。它還可以實現預設的行為,這會在我們講到組合器(combinator)的時候看到。

我們的 trait 實現看起來像下面這樣(這和真實的 futures 實現是一致的):

trait Future {
    type Output;

    fn poll(&mut self, ctx: &Context) -> Poll<Self::Output>;
}

這個 trait 現在還很簡單,只是宣告瞭所需的型別——Output,以及唯一需要的方法的簽名——pollpoll方法持有一個 context 物件的引用。這個物件持有一個對 waker 的引用,waker 被用於提醒執行時(runtime)future 準備好被再次輪詢。

我們的實現

#[derive(Default)]
struct MyFuture {
    count: u32,
}

impl Future for MyFuture {
    type Output = i32;

    fn poll(&mut self, ctx: &Context) -> Poll<Self::Output> {
        match self.count {
            3 => Poll::Ready(3),
            _ => {
                self.count += 1;
                ctx.waker().wake();
                Poll::Pending
            }
        }
    }
}

讓我們一行一行地來看上面的程式碼:

  • #[derive(Default)] 為這個型別自動建立一個::default()函式。數值型別(即這裡的count)預設為 0。

  • struct MyFuture { count: u32 }定義了一個帶有一個計數器(count)的簡單結構體。這讓我們能夠模擬非同步行為。

  • impl Future for MyFuture 是我們對這個 trait 的實現。

  • 我們把 Output 設定為i32型別,因此我們可以返回內部的計數。

  • 在我們的poll實現中,我們基於內部的 count 欄位決定要做什麼、

  • 如果它匹配了 33=>,我們返回一個帶有值為 3 的Poll::Ready響應。

  • 在其他情況下,我們增加計數器的值並且返回Poll::Pending

加上一個簡單的 main 函式,我們可以執行我們的 future 了!

fn main() {
    let my_future = MyFuture::default();
    println!("Output: {}", run(my_future));
}

自己執行一下![10]

最後一步

這就是它的工作原理,但是沒有真正地向你展示出 futures 的強大。所以,讓我們建立一個超級便利的 future,用它來連結到任意任意可以加 1 的型別來進行加 1 操作,例如,MyFuture

struct AddOneFuture<T>(T);

impl<T> Future for AddOneFuture<T>
where
    T: Future,
    T::Output: std::ops::Add<i32, Output = i32>,
{
    type Output = i32;

    fn poll(&mut self, ctx: &Context) -> Poll<Self::Output> {
        match self.0.poll(ctx) {
            Poll::Ready(count) => Poll::Ready(count + 1),
            Poll::Pending => Poll::Pending,
        }
    }
}

這段程式碼看起來複雜但實際上非常簡單。我會再次一行一行地來回顧:

  • struct AddOneFuture<T>(T);這是一個泛型newtype[11]模式的示例。它讓我們能夠wrap其他的結構體並且新增我們自己的行為。

  • impl<T> Future for AddOneFuture<T>是一個泛型 trait 實現。

  • T: Future保證被 AddOneFuture wrap 的任意東西實現了 Future。

  • T::Item: std::ops::Add<i32, Output=i32>確保了Poll::Ready(value)表示的值有對應的+操作。

剩下的部分就很容易看懂了。它使用self.0.poll輪詢內部的 future,貫穿上下文,並且根據結果要麼返回Poll::Pending或者返回內部 future 的計數加 1——Poll::Ready(count + 1)

我們可以只更新main函式以使用我們的新的 future。

fn main() {
    let my_future = MyFuture::default();
    println!("Output: {}", run(AddOneFuture(my_future)));
}

自己執行一下![12]

現在,我們能夠看到我們是如何使用 futures 把非同步行為連結到一起。只需要幾個簡單步驟,就可以建立為 futures 賦予強大能力的鏈式函式(combinators)。

概要

  • Future 是一種利用 Rust 零成本抽象概念來實現良好可讀性、快速的非同步程式碼的強大方式。

  • Futures 行為和 JavaScript 以及其他語言中的 promise 很像。

  • 我們已經學到了很多關於構建通用型別和一部分將行為連結到一起的內容。

接下來

part 2[13],我們將討論組合器(combinators)。組合器,在非技術性方面,能夠讓你使用函式(比如回撥函式)來構建一個新型別。如果你已經用過 JavaScript 的 promises,這些將會很熟悉。

參考資料

[1]

promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

[2]

async/await: https://areweasyncyet.rs/

[3]

tokio: https://tokio.rs/

[4]

此文件: https://tokio.rs/docs/futures/overview/

[5]

future-preview: https://docs.rs/futures-preview/0.3.0-alpha.17/futures/

[6]

Rust book: https://doc.rust-lang.org/stable/book/

[7]

rust playground: https://play.rust-lang.org/

[8]

這裡: https://docs.rs/futures-preview/0.3.0-alpha.17/futures/task/index.html

[9]

Trait: https://doc.rust-lang.org/book/ch10-02-traits.html

[10]

自己執行一下!: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=254b419cb4a9229b67219400890c9e9b

[11]

newtype: https://github.com/rust-unofficial/patterns/blob/master/patterns/newtype.md

[12]

自己執行一下!: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=82df3f3ae9ab242d1d536bf9c851349d

[13]

part 2: https://www.viget.com/articles/understanding-futures-is-rust-part-2/

相關文章