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


原文標題: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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM