做者介紹:Nick Cameron,PingCAP 研發工程師,Rust core team 成員,專一於分佈式系統、數據庫領域和 Rust 語言的進展。html
最近我將一箇中小型的 crate 從 futures 庫的 0.1 遷移至了 0.3 版本。過程自己不是特別麻煩,但仍是有些地方或是微妙棘手,或是沒有很好的文檔說明。這篇文章裏,我會把遷移經驗總結分享給你們。git
我所遷移的 crate 是 TiKV 的 Rust Client。該 crate 的規模約爲 5500 行左右代碼,經過 gRPC 與 TiKV 交互,採用異步接口實現。所以,對於 futures 庫的使用頗爲重度。github
異步編程是 Rust 語言中影響普遍的一塊領域,已有幾年發展時間,其核心部分就是 futures 庫。做爲一個標準 Rust 庫,futures 庫爲使用 futures 編程提供所需數據類型以及功能。雖然它是異步編程的關鍵,但並不是你所須要的一切 - 你仍然須要能夠推動事件循環 (event loop) 以及與操做系統交互的其餘庫。數據庫
futures
庫在這幾年中變化很大。最新的版本爲 0.3(crates.io 發佈的 futures
預覽版)。然而,有許多早期代碼是 futures 0.1 系列版本,且一直沒有更新。這樣的分裂事出有因 - 0.1 和 0.3 版本之間變化太大。0.1 版本相對穩定,而 0.3 版本一直處於快速變化中。長遠來看,0.3 版本最終會演進爲 1.0。有一部分代碼會進入 Rust 標準庫,其中的第一部分已在最近發佈了穩定版,也就是 Future
trait。編程
爲了讓 Rust Client 跑在穩定的編譯器上,咱們將核心庫限制爲僅使用穩定或即將穩定的特性。咱們在文檔和示例中確實使用了 async/await,由於 async/await 更符合工程學要求,並且未來也必定會成爲使用 Rust 進行異步編程的推薦方法。除了在覈心庫中避免使用 async/await,咱們對使用 futures 0.1 的 crate 也有依賴,這也意味着咱們須要常常用到兼容層。從這個角度說,咱們此次遷移其實並不夠典型。安全
我不是異步編程領域的專家,或許有其餘方法能讓咱們此次遷移(以及所涉及的代碼)更符合你們的使用習慣。若是您有好的建議,能夠在 Twitter 上聯繫我。若是您想要貢獻 PR 就更讚了,咱們期待愈來愈多的力量加入到 TiKV Client 項目裏。異步
此類變化是指那些 「查詢替換類」 ,或其餘無需複雜思考的變化。async
這一類別中最大的變化莫過於 0.1 版本的 Future
簽名中包含了一個 Error
關聯類型,並且 poll
老是會返回一個 Result
。0.3 版本里該錯誤類型已被移除,對於錯誤須要顯式處理。爲了保持行爲上的一致性,咱們須要將代碼裏全部 Future<Item=Foo, Error=Bar>
替換爲 Future<Output=Result<Foo, Bar>>
(留意 Item
到 Output
的名稱變化)。替換後, poll
就能夠返回和之前同樣的類型,這樣在使用 futures 的時候無需任何變化。分佈式
若是你定義了本身的 futures,那就須要根據是否須要處理錯誤的需求更新 futures 的定義。異步編程
futures 0.3 中支持 TryFuture
類型,基本上能夠看做 Future<Output=Result<...>>
的替代。使用這個類型,意味着你須要在 Future
與 TryFuture
之間轉換,所以最好仍是儘可能避免吧。TryFuture
類型包含了一個 blanket implementation,這使它能夠經過 TryFutureEx
trait 輕鬆將某些函數應用於此類 futures。
futures 0.3 中,Future::poll
方法會接受一個新的上下文參數。這基本上只須要調用 poll
方法便可完成傳遞(偶爾也會忽略)。
咱們的依賴包依然使用了 futures 0.1,因此咱們必須在兩個版本的庫之間轉換。0.3 版本包含了一些兼容層以及其餘實用工具(例如 Compat01As03
)。咱們在調用依賴關係時會用到這些。
wait
方法已被從 Future
trait 中移除。這是讓人拍手稱快的變化,由於該方法確實夠反人性,並且自己能夠用 .await
或 executor::block_on
代替(須要注意的是後者可能會阻斷整個進程,而並不僅是當前執行的 future)。
futures 0.3 中, Pin
是一個頻繁使用的類型, Future::poll
方法簽名的 self
類型對其尤其青睞。除了對這些簽名進行一些機械性的處理以外,我還得藉助於 Pin::get_unchecked_mut
與 Pin::new_unchecked
這兩種方法(均爲不安全方法)對 futures 的項目字段作一些變動。
指針定位(pinning)是一個微妙又複雜的概念,我至今也不敢說本身已經掌握了多少。我能提供的最好的參考是 std::pin docs。下面是我整理的一些要點(有一些重要的細節此處不會涉及,這裏本意也並不是提供一個關於指針定位的教程)。
Pin
做爲一個類型構造,只有用於指針類型(如 Pin<Box<_>>
)時纔會生效。
Pin 自己是一種「標識/封裝」類型(有一點像 NonNull
),並非指針類型。
若是一個指針類型被「定位」了,意味着指針指向的值不可移動(當一個非拷貝對象經過數值傳入,或者調用 mem::swap
時會發生移動)。須要注意的移動只能發生在指針被定位以前,而非以後。
若是某個類型使用了 Unpin
trait,這意味着不管此類型移動與否都不會有任何影響。換句話說,即便指向該類型的指針沒有被定位,咱們也能夠放心把它看成被定位的。
Pin
與 Unpin
並無置入 Rust 語言,雖然某些特性會對指針定位有間接依賴。指針定位由編譯器強制執行,但編譯器自己卻不自知(這點很是酷,也體現了 Rust 特性系統對此類處理的強大之處)。它是這樣工做的:Pin<P<T>>
只容許對於 P
的安全訪問,禁止移動 P
指向的任何數值,除非 T
應用了 Unpin
(代碼編寫者已宣稱 T
並不在乎是否被移動)。任何容許刪除沒有執行 Unpin
數值的操做(可變訪問)都是 unsafe
的,且應該由程序編寫者決定是否要移動任何數值,並保證以後的安全代碼中不可刪除任何數值。
讓咱們回到 futures 遷移的話題上。若是你對 Pin
使用了不安全的方法,你就須要考慮上面的要點,以保證指針定位的穩定。std::pin docs 提供了更多的解釋。我在許多地方經過字段投射的方式爲另一個 future 調用 poll
方法(有時是間接的),爲了達到這個目的,你須要一個已定位的指針,這也意味着能你須要結構性指針定位。如,你能夠將 Pin<&mut T>
字段投射至 Pin<&mut FieldType>
。
遷移中比較讓人不爽的一點是 futures 庫裏有許多函數(與類型)的名稱改變了。有的名稱和標準庫裏的通用名重複,這讓用自動化的手段處理變動的難度變大。好比,Async
變成了 Poll
,Ok
變成了 ready
,for_each
變成 then
,then
變成 map
,Either::A
變成 Either::Left
。
有時名稱沒有變化,但其表明的功能語義變了(或者兩方面都變了)。一個較爲廣泛的變化就是 closure 函數如今會返回可使用 T
類型生成數值的 future,而不會直接返回數值自己。
有許多組合子函數從 Future
trait 移至擴展 crate 裏。這個問題自己不難修復,只是有時候不容易從錯誤信息中斷定。
0.1 版本的 futures 庫包含了 LoopFn
這個 future 構造,用於處理屢次執行某動做的 futures。LoopFn
在 0.3 版本中被移除,這樣作的緣由我的認爲多是 for
循環自己是 async
的函數,或者 streams 纔是長遠看來的更佳解決方案。爲了讓咱們的遷移過程簡單化,我爲 futures 0.3 寫了咱們本身版本的 LoopFn
future,其實大部分也都是複製粘貼的工做,加上一些調整(如處理指針定位投射):code。後來我將幾處 LoopFn
用法轉換爲 streams,對代碼彷佛有必定改進。
咱們在項目中幾個地方使用了 sink。我發現對於它們對遷移和 futures 相比要有難度很多,其中最麻煩的問題就是 Sink::send_all
結構變了。0.1 版本里,Sink::send_all
會獲取 stream 的全部權,並在肯定全部 future 都完成後返回 sink 以及 stream。0.3 版本里, Sink::send_all
會接受一個對 stream 的可變引用,不返回任何值。我本身寫了一個 兼容層 在 futures 0.3 裏模擬 0.1 版本的 sink。這不是很難,但也許有更好的方式來作這件事。
你們能夠在 這個 PR 裏看到整個遷移的細節。本文最初發表在 www.ncameron.org。