深入瞭解 Rust 異步開發模式

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"異步開發應該是從 python 和 nodejs 開始的,帶來了極大的性能提升。Rust 充分利用異步模型的框架,在Web Framework Benchmark 性能榜單上,長期排名前十。異步不光帶來性能的提升,還有很多更好的機制。我們通過 Tokio 和 async-std 兩個 Rust 異步框架,來學習一下異步開發模型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"先了解 std::Waker"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"task 就是未來要執行的任務,稱爲異步任務。對於異步的操作,有一個點,就是能夠控制任務。讓任務能夠在合適的時候啓動,以及在沒有事的時候 pending。爲了達到這個目標,rust 設計了 Waker 這個機制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Waker 包含以下幾個內容:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Context : 上下文,在這裏,就是包裝 Waker 的入口"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RawWaker : 生成 Waker"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RawWakerVTable : 是一個虛函數指針表(Virtual Function Pointer Table, vtable),指向 RawWaker 對象的指針的列表"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Waker : 通知任務準備啓動的入口"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上幾個的關係是這樣:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e2/e214b136cc26d52906cb80f46f651057.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Context 結構裏面定義一個 Waker,實現了和 Waker 的相互轉換。RawWaker 定義了一個指向任何數據類型的指針,和虛函數表,用來實現和虛函數的綁定、RawWakerVTable 定義了虛函數的幾種操作,其中wake 和 wake_by_ref 和Waker 的實例對應"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Waker 裏面有兩個重要的操作:wake() 和 clone()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"wake()的代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"pub fn wake(self) {\n // The actual wakeup call is delegated through a virtual function call\n // to the implementation which is defined by the executor.\n let wake = self.waker.vtable.wake;\n let data = self.waker.data;\n\n // Don't call `drop` -- the waker will be consumed by `wake`.\n crate::mem::forget(self);\n\n // SAFETY: This is safe because `Waker::from_raw` is the only way\n // to initialize `wake` and `data` requiring the user to acknowledge\n // that the contract of `RawWaker` is upheld.\n unsafe { (wake)(data) };\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"wake() 的作用是讓 task 再次啓動,並且獲得 pending 之前的信息(堆棧和函數操作)。這個代碼裏面的 vtable 和 data 就是 RawWaker 裏面的數據。中間有一個操作 mem::forget(self),forget 的作用是讓 self 實例進入一個不能操作的狀態,但是實例還在,可以下次調用。forget 是調用了 mem crate 裏面 ManuallyDrop::new() 。ManuallyDrop 能夠控制編譯器,讓其不要自動去調用實例的解構函數 (destructor),這樣就能保存之前函數運行的信息。forget 調用 ManuallDrop::new() ,就是把 self 實例創建成一個 ManuallyDrop,獲得之前 Waker 的信息,這樣在切換到另外一個runtime 或者 thread 的時候,之前的 Waker 信息就同步過去,不會隨着進程結束而解構。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"clone() 的代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"impl Clone for Waker {\n #[inline]\n fn clone(&self) -> Self {\n Waker {\n // SAFETY: This is safe because `Waker::from_raw` is the only way\n // to initialize `clone` and `data` requiring the user to acknowledge\n // that the contract of [`RawWaker`] is upheld.\n waker: unsafe { (self.waker.vtable.clone)(self.waker.data) },\n }\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"clone() 的作用是 clone 這個 waker ,這個用在 task 在要進入 Pending 之前,把獲得數據 clone 一份到一個全局的數據結構裏面,下一次調用的時候,不會丟失。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼會設計一個 Waker ? 在 task 進入 pending 的時候用一個全局的方法來保存狀態,下次啓動再調用不就可以了嗎?我猜測主要是爲了靈活的和性能。還有一個原因,task 很多時候是以閉包的形式出現,閉包可以獲取所在進程的環境變量,但是自己沒有 heap allocation 。這樣的話,其實就很難通過全局或者參數傳遞的方法來實現。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Withoutboats 有一篇文章:"},{"type":"link","attrs":{"href":"https://boats.gitlab.io/blog/post/wakers-i/","title":null},"content":[{"type":"text","text":"What does a waker do?"}]},{"type":"text","text":"。說了一個意思是,在異步過程中,有一個 Poll(這個下一篇會講到),傳入 Future 的值(data和vtable)會連續傳遞,就是 Poll 的第二個參數,Context,裏面實際是 Waker。直到有一個事件觸發 Future 重新啓動,這個值也重新使用。但是這個傳遞過程是動態的,因爲也可能是多線程的的等待獲取。所以重新設計了 Waker。Waker 有一個關鍵操作, Clone()。在事件觸發,Waker 啓動的時候,一定要 impl Clone。還是上面的原因,Waker 存在的運行時可能動態的,也可能是多派發的。所以 Waker 必須要自我 clone。然後 Clone 操作的返回必須是 Waker,而不是 self。也就是重新建立一個 Waker,而不是引用到之前的 Waker。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總結一下,Waker 是爲了支持異步任務的靈活控制而設計的。讓任務能夠在需要的時候啓動,和空閒的時候停止。那任務是什麼呢?我們需要理解兩個概念 Future 和 Task,讓我們先理解一下 Future 是什麼?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"task 是 rust 的 libcore 的模塊,也就是說 task 是 rust 的核心庫"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Future :異步的核心概念"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Future 意思就是未來要執行的動作 。按照 Aaron Turon 的定義,future 本質上表達一個還沒有準備好的值。通常,future 的完成(值準備好了)取決於事件在某處完成了某個事件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"In essence ,afuture represents a value tha might not be ready yet. Usually, the future becomes complete (the value is ready ) due to an event happening somewhere else."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://aturon.github.io/blog/2016/09/07/futures-design/","title":null},"content":[{"type":"text","text":"原文"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"future 這個 trait 定義如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"pub trait Future {\n type Output;\n fn poll(self:Pin,cx: &mut Context ) -> Poll<:output>;\n}\nenum Poll {\n Ready(T),\n Pending,\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Output 是 Future 完成之後的類型 poll 有兩個狀態 Ready 和 Pending ,當 future 還沒有完成,poll 返回 Pending 狀態。同時啓動一個 waker,記錄到 event loop(或者 reactor) 裏面。當操作 Ready 的時候,event loop 再次調用 poll,返回 ready ,然後 runtime 會啓動一個 event loop 和線程來執行操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 future 在 Ready 的狀態容易理解,就完成後續操作就行。但是 poll 在 Pending 的狀態下就面臨一些問題。因爲 future 在執行到 pending 的時候,需要停下來,然後就像 Aaron Turon 說的,在其他某個事件完成之後,再啓動這個 future 執行後續的動作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這就需要保存 future 的狀態,然後在之後再啓動。定時啓動的機制是通過 waker 裏面的 mutex + condvar 來實現的,在 tokio 裏,包裝成了 park/unpark 。這個在後面一章詳細描述。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"保存 future 的狀態,就需要保存 future 的的相關信息,包括引用的數據。但是在 future 停止再啓動的時候,程序的運行空間會變化,例如到了另外一個線程。引用就指向了不對的位置,導致問題。這個問題在 rust 裏面,用pin/unpin來解決。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Future 裏面的 poll 。有兩個 Impl,針對 Unpin 和 Pin 類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"impl Future for &mut F {\n type Output = F::Output;\n\n fn poll(mut self: Pin, cx: &mut Context) -> Poll<:output> {\n F::poll(Pin::new(&mut **self), cx)\n }\n}\n\nimpl

Future for Pin

\nwhere\n P: Unpin + ops::DerefMut,\n{\n type Output = <

::Target as Future>::Output;\n\n fn poll(self: Pin, cx: &mut Context) -> Poll<:output> {\n Pin::get_mut(self).as_mut().poll(cx)\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Pin 類型能夠讓數據在內存中固定。withoutboats 有一篇 "},{"type":"link","attrs":{"href":"https://boats.gitlab.io/blog/post/2018-01-25-async-i-self-referential-structs/","title":null},"content":[{"type":"text","text":"blog"}]},{"type":"text","text":",詳細說明了自引用結構在 MOVE 之後產生的問題。Pin 能夠解決這類問題。在上面的代碼裏面,看到針對 unpin 的類型,也增加了 Pin::new 來保障數據在內存中固定"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Future 的 poll 的第二個參數是 Context 類型,也就是 waker 的封裝。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"fn poll(self: Pin, cx: &mut Context) -> Poll<:output>;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個 Context 用來保存 future 的狀態,在 poll 是 Pending 的時候保存 future 的信息和狀態,在 poll 到 Ready 的時候,再執行。所以在每次 poll 之後,如果發現不是 Ready 的狀態,都會重新把這個 Context 帶入到一個新的 future 等待下次事件觸發調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Future 和 Pin 構成了 rust async/await 的基礎。在函數前面加上 async ,就把函數包裝稱爲了一個 Future;Future 後面加上 .await,就執行 Future 的 poll 操作。例如:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"async fn read_file(path: &str) -> io::Result {\n let mut file = File::open(path).await?;\n let mut contentx = String::new();\n file.read_to_string(&mut contexts).await?;\n Ok(contents)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"async 在函數前面,把函數包裝爲一個"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"Future>"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在函數內部,也有兩個 Future 的執行。一個是 File::open,一個是 read_to_string。這個又帶來一個問題,就是 Future 裏面包含了 Future ,是怎麼執行的。按照程序邏輯,應該是要執行完 File::open 之後才能繼續後面的操作,也就是說 Future 要按照順序執行裏面的 Future,也就是說 Future 的執行要支持嵌套和組合使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Future 的嵌套組合也存在幾個情況,在 async-std 裏面,總結了這麼幾種:join、race、try_join、try_race、flatten 和 delay"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Join 比較容易理解。有兩個 Future ,L 和 R 。先檢查 L 是不是 Ready,如果 Ready ,再檢查 R 的 Output 是不是有值(並沒有 Poll L)。如果是,則把 L 和 R 的 Output 組合成一個 tuple 作爲 Join 之後的 Output,然後返回 Poll::Ready 狀態"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TryJoin 和 Join 類似。先檢查 L 是不是 Ready,然後檢查 L 的 Output 是不是有錯誤,如果有錯誤,就返回Err。然後檢查 R 的 Output 是不是有值,如果有值,就把 L 和 R 的 Output 合併返回 Poll:Ready。和 Join 相比,加了一步,檢查 L 的 Output 是不是有錯誤"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Race 是兩個比誰更快的意思。先檢查 L 是不是 Ready,如果Ready,就去 執行 L。然後對 R 再做同樣的操作。如果其中有一個完成,就算 Race 完成,另外一個就不管了。Future 退出。當然,這個裏面,L 佔有一點先發優勢,因爲先執行 L"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TryRace 和 TryJoin 類似,就不在重複描述"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flattern :第一個 Future 的輸出,是第二個 Future 的輸入。也就是嵌套 Future 。類似這樣: async{ async {1} }。執行順序就是先執行第一個,然後有返回結果之後。把返回結果再生成一個 Future,繼續執行。然後返回最後的結果。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Delay :就是延遲執行的 Future,可以爲 Future 設置程序到了之後再過一段時間執行"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然, Future 不止針對兩個的組合。也有針對 3、4 甚至多個組合。Tokio 就有一個 try_join3的組合。組合方式也不止這幾種,crate.io 裏面有一些 crates ,針對 Flattern ,還有 FlatternSink 和 FlatternStream,以及其他組合方式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看到這裏,可能會有些迷惑。還記得之前提到的 ,Future 是未來要執行的動作。這些組合以及執行順序,就有點類似編織未來程序執行的路徑和方式。但是 Future 如果不被 Poll 或者 .await,Future 是靜態的。這個和我們平時寫同步執行代碼有相似,也有不同。不同的地方在於,如果要獲得一箇中間判斷的狀態,就需要 Poll。如果要根據輸入做動作,也需要在 Poll 的下面寫對應的 match。感覺上會比較複雜。好在 Rust 提供 async / .await 的方式,能夠讓我們按照同步順序操作的思路來寫,就像上面描述的 read_file 函數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"剛纔提到,Future 是未來要執行的動作。那怎麼執行 Future,就需要在代碼裏調用 Poll,或者在函數後面增加 .await 。Future 的設計是爲了提供異步執行的模式,爲什麼會有異步執行的模式,主要是爲了性能。能夠按照計算機的運行方式,採用事件觸發,把任務切成小的操作步驟,更高效的調度,來達到高性能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果要設計一個高效完整的異步框架。在 Future 和 Waker 的基礎上,還需要提供幾個部分:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"executor : 執行任務的基礎。線程、協程、進程或者其他計算運行時"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"scheduler:調度方式,針對不同的計算調度不同的 executor 來運行"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"park : 對線程進行管理,滿足 scheduler 的調度要求"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"task :Future 是小片的執行操作,要完整一個完整的任務,需要更強大的方式,就是task"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面就以 Tokio 爲例來說明以上幾個模塊"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"executor : 爲什麼用 Native Thread"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作爲異步併發模式的基礎,採用線程、Green Thread 還是 Coroutine,從可行性上都可以。Rust 採用的是 Native Thread。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼會採用 Native Thread ,什麼是 Native Thread ? Rust Team 核心成員 Steve Klabnik ( @withoutboats ) 有"},{"type":"link","attrs":{"href":"https://www.infoq.com/presentations/rust-2019/","title":null},"content":[{"type":"text","text":"一個演講"}]},{"type":"text","text":"詳細說明了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"核心的原因是,Rust 是 \"system programming language\" ,和 C 之間不能有 overhead 。也就是說,Rust 必須使用系統 Native 的 Thread,才能和 C 的轉換沒有額外的 IO 損耗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 的 Async 採用了一種 \"Synchronous non-blocking network I/O\" (同步非阻塞 IO)。這個看上去有些矛盾,但是仔細瞭解一下,感覺挺有道理的。同步阻塞的問題,就是效率較低。異步非阻塞的問題,對於長耗時的操作效率較低。異步阻塞,能夠讓長耗時的任務安排到獨立線程運行,達到更好的性能。同步非阻塞IO,就是用同步的方法來寫代碼,但是內部其實是異步調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"async-std 在"},{"type":"link","attrs":{"href":"https://async.rs/blog/stop-worrying-about-blocking-the-new-async-std-runtime/","title":null},"content":[{"type":"text","text":"這篇博客"}]},{"type":"text","text":" 這樣說:\"The new runtime "},{"type":"text","marks":[{"type":"strong"}],"text":"detects blocking"},{"type":"text","text":" automatically. We don’t need "},{"type":"link","attrs":{"href":"https://docs.rs/async-std/1.2.0/async_std/task/fn.spawn_blocking.html","title":null},"content":[{"type":"text","text":"spawn_blocking"}]},{"type":"text","text":" anymore and can simply deprecate it \" 。系統 runtime 竟然能夠自動檢測是不是阻塞操作,不需要顯式調用 spawn_blocking 來針對阻塞操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是 Native Thread 在應對 IO 請求的時候,存在問題。它會針對每個請求,準備一個線程。這樣會極大消耗系統資源,並且這些線程在等待的時候什麼都不做。這樣的機制面對大量請求的異步操作時會非常低效。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Go 和 Erlang 都是採用 Green Thread 來解決這個問題。但是 Rust 因爲不想和 C 之間有更多的隔閡,不想採用 Green Thread 模式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 參考了 Nginx 的 Event Poll 模型,還有 Node.js 的 \"Evented non-blocking IO\" 模型。withoutboats 非常推崇 Node.js 模型,但是 Node.js 帶來了回調地獄 (callback hell) 。Javascript 又創造了 Promise 來避免回調的一些問題。Promise 就是 Future 的思路來源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Twitter 的工程師在處理這個問題的時候,放棄啦 JVM 轉而用 Scala ,獲得了非常大的性能提升 。然後他們寫了一個 Paper 叫做 \""},{"type":"link","attrs":{"href":"https://monkey.org/~marius/funsrv.pdf","title":null},"content":[{"type":"text","text":"Your Server as a Function"}]},{"type":"text","text":"\" 。介紹了一個概念,叫做 Future 。這樣描述:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"A future is a container used to hold the result of an asynchronous operation such as a network RPC, a timeout, or a disk I/O opera- tion. A future is either "},{"type":"text","marks":[{"type":"italic"}],"text":"empty"},{"type":"text","text":"—the result is not yet available; "},{"type":"text","marks":[{"type":"italic"}],"text":"suc- ceeded"},{"type":"text","text":"—the producer has completed and has populated the future with the result of the operation; or "},{"type":"text","marks":[{"type":"italic"}],"text":"failed"},{"type":"text","text":"—the producer failed, and the future contains the resulting exception"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 在這個基礎上,完善並推出了 zero cost future 。就是上面一篇講述的內容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"\"Synchronous non-blocking network I/O \" 是怎麼實現的呢?這裏面的核心是調度 (scheduler),就是讓你用同步的方式來寫代碼,但是內部卻是用異步調用的方式在運行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下一篇就說一下調度 ( scheduler )"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"scheduler :讓異步運作更有效率"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Scheduler 的核心是調度,讓 Future 和 Task 在 Executor 裏面運行的更有效率。Tokio 和 Mio 的核心工程師 Carl Lerche 在 Tokio Blog 寫了一篇文章 : "},{"type":"link","attrs":{"href":"https://tokio.rs/blog/2019-10-scheduler/","title":null},"content":[{"type":"text","text":"Making the Tokio scheduler 10x faster"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文章描述了怎麼優化 work-stealing 的任務調度機制,達到10倍的加速。優化的核心是針對任務調度的消息隊列。最開始的 tokio 採用 "},{"type":"link","attrs":{"href":"https://github.com/crossbeam-rs/crossbeam","title":null},"content":[{"type":"text","text":"crossbeam"}]},{"type":"text","text":" 的消息隊列,是一種“single producer , multi-consumer”的模式。Tokio 參考 Go 優化成 \"multi-producer , single-consumer\" 模型,並且增加了一個 Global Queue,提升了調度效率"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們從 runtime 入手來看 scheduler 是怎麼實現的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8a/8ac310b29da224a84e27b7437a8fb012.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"builder 有兩組 threads,一組是 core_threads,默認是和 CPU 的核數一樣。一組是 max_threads , 默認是512 。Core_threads 是作爲 tokio runtime 的主要 executor。max_thread 是作爲 blocking_pool 的 executor。在 runtime 啓動的時候,core_threads 和 blocking_thread 都啓動。在運行 Future 的時候,tokio::spawn ,在 core_threads 運行; tokio::block_on ,在 blocking_thread 啓動。當然兩種情況線程調度的機制都不一樣"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏面有三種調度方式: shell、basic_scheduler 和 threaded_scheduler"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Shell 沒有實際使用,主要是 basic 和 threaded 兩種。我們先來看一下 basice_scheduler。 BasicScheduler 的結構如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"pub(crate) struct BasicScheduler

\nwhere \tP: Park,\n{\n /// Scheduler component\n scheduler: Arc,\n /// Local state\n local: LocalState

,\n}\nstruct LocalState

{\n /// Current tick\n tick: u8,\n /// Thread park handle\n park: P,\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LocalState 包含兩個比較重要的點, tick 和 park。Park 是針對 Waker 的再次封裝,爲了能夠更好控制線程,這個我們後面單獨解釋。 tick 是針對 task state 的增強,一次 tick 可能包含很多次 task 。例如在讀取 socket 的時候,可以執行很多 task ,每個 task 讀取一小段數據,執行多次,讀完整個數據。在 basic_scheduler 裏面,MAX_TASKS_PER_TICK 默認設置爲 61"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在使用 Tokio 的時候,往往是這樣用 :"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"use tokio::net::TcpListener;\nuse tokio::prelude::*;\n\n#[tokio::main]\nasync fn main() -> Result> {\n let mut listener = TcpListener::bind(\"127.0.0.1:8080\").await?;\n\n loop {\n let (mut socket, _) = listener.accept().await?;\n\n tokio::spawn(async move {\n let mut buf = [0; 1024];\n\n // In a loop, read data from the socket and write the data back.\n loop {\n let n = match socket.read(&mut buf).await {\n // socket closed\n Ok(n) if n == 0 => return,\n Ok(n) => n,\n Err(e) => {\n eprintln!(\"failed to read from socket; err = {:?}\", e);\n return;\n }\n };\n\n // Write the data back\n if let Err(e) = socket.write_all(&buf[0..n]).await {\n eprintln!(\"failed to write to socket; err = {:?}\", e);\n return;\n }\n }\n });\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最開始的 #[tokio::main] 是一個宏,宏下面的 async 函數,是作爲宏的 Future 輸入。tokio::main 做的主要工作是 builder 一個 runtime , 然後啓動 block_on 函數,把 Future 包裝進入 Block_on 。具體流程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/32/32f0a495c2f5a1e5d230a93178e256dc.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"block_on 主要做的工作就是獲得 Future 然後執行。在 Tick Local State 這一步,實際是循環執行了多次 Task,如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/db/dbad697cfd0a801238a8271655b63ecf.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就像上面說的,持續的執行 Task ,到沒有 Task。或者執行 MAX_TASK_PER_TICK 次"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在剛纔的 TcpListener 裏面,有一個 Tokio::spawn(future),spawn 的流程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4f/4fb35a7c2490ed1eb2b0da660516f667.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓動一個任務,然後判斷是不是有 Scheduler 存在。有的話,就 push future 到 Local 的任務隊列,沒有就 push 到 remote 的隊列。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有一個關鍵的地方,就是 Scheduler 有一個 Mpsc (multi-producer, single consumer) 的隊列。還記得這結剛開始的時候那篇文章: "},{"type":"link","attrs":{"href":"https://tokio.rs/blog/2019-10-scheduler/","title":null},"content":[{"type":"text","text":"Making the Tokio scheduler 10x faster"}]},{"type":"text","text":"。爲了提升性能,tokio 優化了 crossbeam 的 queue。這個 Mpsc 有兩個 equeue :Local 和 Remote,Local 是自己線程的任務, Remote 是其他線程推送過來的任務。 在 tick 裏面 獲取 next_task 的時候,有一個邏輯,每一段次數(CHECK_REMOTE_INTERVAL,默認是13) 之後,就去獲取一次 Remote queue 裏面的任務。另外,在 spawn future 的時候,如果 scheduler 不存在,就推送 task 到 remote queue"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Thread_Scheduler 和 Basic_Scheduler 的區別在於, Thread 是多線程多任務模式,Basic 是單線程多任務模式。Threaded 用和 CPU 核數一樣的線程數,針對每個任務用一個線程來執行。block_on 和 spawn 都是這樣。所以 Thread 的流程較爲簡單,沒有 Basic 複雜。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了這個之外,還有一個針對長耗時的 blocking 操作的 spawn_blocking 。就是用在 builder 裏面 create_blocking_pool (512個) 來執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上內容基本描述了 Scheduler 的流程。但是還有很多地方不清晰,因爲我們還有兩個重要的內容沒描述:Park 和 Task 。Park 是在 Waker 基礎上的增強,針對線程狀態和 Future 做更細節的管理和控制; Task 是連接 Future 和 線程模型的重要控制模塊。也可以說是 Rust 異步模式之後開發時接觸最多的概念。按照 Aaron Turon 的定義, Task 是正在執行的 Future ( a task is a future thas is being executed)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先說 Park/Unpark,之後詳細描述 Task"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Park/Unpark : 管理線程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面的 Basic 和 Threaded 裏面,都設計到線程的管理。在 tokio::main 開始的時候,啓動了 cpu_cores 的 core Threads 和 512 個 Thread Pool。這些線程怎麼管理,達到 Scheduler 的要求。Park 起到了關鍵作用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先描述一下 Park 的內部關係:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c7/c7e2d3d4d838807e81b85b7fc6bcfc4b.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Park Trait 定義了一個關於狀態切換的關係,如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/14/149303cede74f4d3f52f21107739ce4d.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Thread 裏面,有三個狀態,Empty、PARKED 和 NOTIFIED。通過 park() 和 unpark() 轉換狀態。在 Inner 這個 struct 和它的 impl 裏面。有一個 Condvar 是 std::sys::condvar,是一個條件變量。條件變量的官方描述是這樣:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Condition variables represent the ability to block a thread such that it consumes no CPU time while waiting for an event to occur. Condition variables are typically associated with a boolean predicate (a condition) and a mutex. The predicate is always verified inside of the mutex before determining that a thread must block."}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"條件變量能夠阻塞一個線程,讓它不消耗 CPU。直到一個事件觸發,線程再繼續執行。條件變量往往和一個 bool類型及一個 mutex 關聯,這個 bool 類型包裝在 mutex 裏面,用來確認這個線程是不是要阻塞。condvar 的內部實現利用了 sys::condvar ,這是一個操作系統層的條件變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 park() 裏面,有一個 self.condvar.wait ,會讓線程 block ,等待 condvar 調用 notify 。在 unpark() , 除了設置 state 爲 NOFITIED 外,還調用了了 self.condvar.notify_one() ,讓線程重新激活。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Thread 除了 ParkThread 和 UnparkThread 之外,還有一個 CachedParkThread,用在多線程 blocking 操作裏面,就是上面一篇說過的 spawn_blocking 。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Task 把一切都連起來"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Tokio 的文檔裏說,Task 是輕量級、非阻塞的執行單元。在 Tokio Task 的代碼說,Task 是異步綠色線程(Asynchronnouse green-threads)。 Task 類似於線程,但是被 Tokio::Runtime 管理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Task 的結構如下:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/80/802f29fcb7b0b87f7fc0840cd9642111.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/99/99861ff6a1bcb007401955e045a9dbe3.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這兩頁結構圖還不是 Task 的全部,Task 還包含了 join、list、local、harness、queue、stack 等10多個 Struct 和 Trait 。我們嘗試換一個方式,通過 Task 的實際使用來理解 Task 的機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照 Tokio 文檔的案例:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"use tokio::net::TcpListener;\nuse tokio::prelude::*;\n\n#[tokio::main]\nasync fn main() -> Result> {\n let mut listener = TcpListener::bind(\"127.0.0.1:8080\").await?;\n loop {\n let (mut socket, _) = listener.accept().await?;\n tokio::spawn(async move {\n loop {\n // ... do somethings ...\n }\n });\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面說過,tokio::main 是啓動了 runtime::block_on , 然後把 async 後面的內容作爲一個 Future 傳給 block_onBlock_on , 如果是 Basic Schedule ,則按照 Basic Schedule 開始循環執行 Future.poll 。 流程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9c/9ccb213b8820fce202330885bdcde253.jpeg","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 aync 後面,又調用了 tokio::spawn 一個新的 task ,代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"pub(crate) fn spawn(&self, future: F) -> JoinHandle<:output>\n where\n F: Future + Send + 'static,\n F::Output: Send + 'static,\n {\n let (task, handle) = task::joinable(future);\n self.scheduler.schedule(task);\n handle\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"spawn 一個新的 Future ,包裝成 joinable task。JoinHandle 包裝了 Task,讓 task 能夠在 future lifetime 結束還能保留。JoinHandle 在 Drop Trait 裏增了邏輯"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"rust"},"content":[{"type":"text","text":"impl Drop for JoinHandle {\n fn drop(&mut self) {\n if let Some(raw) = self.raw.take() {\n if raw.header().state.drop_join_handle_fast() {\n return;\n }\n\n raw.drop_join_handle_slow();\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果 Task 可以快速釋放,就調用 drop_join_handle_fast,否則就是 drop_join_handle_slow。這個在 task::state 裏面實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的 Schedule.tick, 在 前面 Scheduler 的時候描述過,多次調動 Task ,可以包裝成一個 tick 來返回。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們簡單描述了一下 Task 以及 Task 的使用。至此,我們可以說囫圇吞棗的把 Tokio 的內核和機制瞭解了一下。Tokio 還在發展 ,Rust 也還在進化。但是投入 Rust 及學習 Tokio ,還是非常有意義,希望我們都能堅持下去。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這篇文章是春節疫情的時候寫的,短短半年。Tokio 和 Rust 異步生態已經有了更大的進步,下一次,我們嘗試瞭解一下 Tokio 最新進展,並且通過 Tokio 來嘗試構建一個全異步數據處理平臺"}]}]}]}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章