使用Rustlang的Async Tokio執行時處理CPU密集型任務
Rust 內建了對非同步 ( async) 程式設計模型的支援,類似於 JavaScript 等語言。
要充分利用多核和非同步 I/O,必須使用執行時,雖然 Rust 社群有多種替代方案,但 Tokio 是事實上的標準。
CPU 密集型計算定義:以消耗大量 CPU 用於儲存重組、預先計算各種索引或直接回答客戶端查詢的方式處理資料。
這些計算通常被分解成許多獨立的塊,稱為“任務”,然後並行執行以利用現代 CPU 中可用的許多核心。
確定何時執行哪些任務通常由稱為“任務排程程式”的東西完成,它將任務對映到可用的硬體核心/作業系統執行緒。
當在 Rust 生態系統中尋找任務排程器時,就像InfluxDB IOx 和 DataFusion 所做的那樣,你自然會選擇 Tokio,它看起來很不錯:
- 您已經擁有 Tokio(沒有新的依賴項)。
- Tokio 實現了一個複雜的工作竊取排程程式。
- Tokio 有效地內建了對延續 ( async/ await) 的語言支援,以及許多相對成熟的流、非同步鎖定、通道、取消等庫。
- Tokio 以在整個 Rust 生態系統中經過良好測試和大量使用而聞名。
- Tokio 通常會在同一個執行器執行緒上執行任務和未來,這對於快取區域性性非常有用。
- Tokio 有據可查,積極維護,並且一直在變得更好。
但是:
舊版本的 Tokio 文件(例如1.10.0)著名的告誡:
“如果您的程式碼受 CPU 限制,並且您希望限制用於執行它的執行緒數,您應該在另一個執行緒池(例如 Rayon)上執行它。”
這個宣告在廣泛的 Rust 社群中都引起了嚴重的混亂。多人讀到它的意思是Runtime永遠不應該將 Tokio 用於 CPU 密集型任務,關鍵實際上是同一個Runtime 例項(同一個執行緒池)不應該同時用於 I/O 和 CPU。
順便說一句,Tokio 文件建議將Rayon用於 CPU 密集型任務。Rayon 是許多應用程式的絕佳選擇,但它不支援 . async,因此如果您的程式碼必須執行任何 I/O,您將不得不跨越痛苦的同步/非同步邊界。我還發現對映一個基於拉取的執行模型具有挑戰性,其中任務必須等待所有輸入準備好才能執行到 Rayon。
智者說,“使用 Tokio 進行 CPU 密集型工作會增加你的請求尾部延遲,這是不可接受的。”
請求尾部會影響是否活著的檢查指標,對於使用容器編排系統(Kubernetes)部署的系統來說很重要,因為 Tokio 正在有效地充分利用您的 CPU 來處理大量資料處理任務,那麼 Kubernetes 將無法獲得所需的“一切正常”響應並終止您的程式。
這種推理得出了一個經典結論,即由於尾部延遲很關鍵,因此您不能將 Tokio 用於 CPU 繁重的任務。
然而,正如 Tokio 文件所建議的那樣,在 CPU 完全飽和的同時避免被 Kubernetes 和朋友攻擊,真正重要的是使用一個單獨的執行緒池——一個用於“延遲很重要”的任務,例如響應/health,另一個用於 CPU繁重的任務。這些執行緒池的最佳執行緒數因您的需要而異,這是另一篇單獨文章的好主題。
也許透過將 TokioRuntime視為一個複雜的執行緒池,使用不同Runtime例項的想法可能看起來更可口。
如何將 Tokio 用於 CPU 密集型任務?
這是我們如何在 InfluxDB IOx 中的單獨 Tokio Runtime 上執行任務的簡化版本。(完整版本可以在我們的 repo中找到,並且有額外的乾淨關閉和加入邏輯。)
pub struct DedicatedExecutor { state: Arc<Mutex<State>>, } /// Runs futures (and any `tasks` that are `tokio::task::spawned` by /// them) on a separate Tokio Executor struct State { /// Channel for requests -- the dedicated executor takes requests /// from here and runs them. requests: Option<std::sync::mpsc::Sender<Task>>, /// Thread which has a different Tokio runtime /// installed and spawns tasks there thread: Option<std::thread::JoinHandle<()>>, } impl DedicatedExecutor { /// Creates a new `DedicatedExecutor` with a dedicated Tokio /// executor that is separate from the threadpool created via /// `[tokio::main]`. pub fn new(thread_name: &str, num_threads: usize) -> Self { let thread_name = thread_name.to_string(); let (tx, rx) = std::sync::mpsc::channel::<Task>(); let thread = std::thread::spawn(move || { // Create a new Runtime to run tasks let runtime = Tokio::runtime::Builder::new_multi_thread() .enable_all() .thread_name(&thread_name) .worker_threads(num_threads) // Lower OS priority of worker threads to prioritize main runtime .on_thread_start(move || set_current_thread_priority_low()) .build() .expect("Creating Tokio runtime"); // Pull task requests off the channel and send them to the executor runtime.block_on(async move { while let Ok(task) = rx.recv() { Tokio::task::spawn(async move { task.run().await; }); } let state = State { requests: Some(tx), thread: Some(thread), }; Self { state: Arc::new(Mutex::new(state)), } } |
此程式碼建立一個 new std::thread,它建立一個單獨的多執行緒 TokioRuntime來執行任務,然後從Channel讀取任務並將spawn它們傳送到 new Runtime上。
注意:新執行緒是關鍵。如果您嘗試Runtime在主執行緒或 Tokio 建立的執行緒之一上建立一個新執行緒,您將收到錯誤訊息,因為已經安裝了一個Runtime。
這是向第二個Runtime傳送任務的相應程式碼。
impl DedicatedExecutor { /// Runs the specified Future (and any tasks it spawns) on the /// `DedicatedExecutor`. pub fn spawn<T>(&self, task: T) -> Job<T::Output> where T: Future + Send + 'static, T::Output: Send + 'static, { let (tx, rx) = tokio::sync::oneshot::channel(); let fut = Box::pin(async move { let task_output = task.await; tx.send(task_output).ok() }); let mut state = self.state.lock(); let task = Task { fut, }; if let Some(requests) = &mut state.requests { // would fail if someone has started shutdown requests.send(task).ok(); } else { warn!("tried to schedule task on an executor that was shutdown"); } Job { rx, cancel } } } 上面的程式碼使用了一個Future被呼叫的包裝器Job,它處理將結果從專用執行器傳輸回主執行器,如下所示: #[pin_project(PinnedDrop)] pub struct Job<T> { #[pin] rx: Receiver<T>, } impl<T> Future for Job<T> { type Output = Result<T, Error>; fn poll( self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll<Self::Output> { let this = self.project(); this.rx.poll(cx) } } |
可以在這個Github gist中找到所有程式碼。
相關文章
- 使用 setTimeout 拆解一些 CPU 密集型的執行任務
- Spring Boot @Async 非同步任務執行Spring Boot非同步
- Java多執行緒並行處理任務的實現Java執行緒並行
- 前端多執行緒處理——async/await前端執行緒AI
- Spark 叢集執行任務失敗的故障處理Spark
- Spring Boot使用執行緒池處理事務任務Spring Boot執行緒
- Java中的任務超時處理Java
- HarmonyOS CPU與I/O密集型任務開發指導
- python apscheduler定時任務處理Python
- 如何使用cron任務每隔2天在固定時間執行任務
- laravel框架任務排程(定時執行任務)Laravel框架
- 為什麼說IO密集型業務,執行緒數是CPU數的2倍?執行緒
- SpringBoot執行定時任務@ScheduledSpring Boot
- Rust 世界中主流的非同步執行時效能測試 Tokio/Tokio-uring/MonoIO/GlommIORust非同步Mono
- 讓CPU對多個任務輪流交替執行
- PHP定時執行任務的實現PHP
- 語音社交原始碼重啟,正在執行的任務會如何處理?原始碼
- Springboot-之定時任務,啟動執行任務Spring Boot
- 後臺程式在處理繁重的任務時,呼叫外部程式非同步執行的簡單實現非同步
- uniGUI釋出時執行時包的處理GUI
- Loom會造成CPU密集型執行緒的不公平排程OOM執行緒
- 聊聊專案中定時任務的處理方式
- Spring Boot中有多個@Async非同步任務時,記得做好執行緒池的隔離!Spring Boot非同步執行緒
- 使用screen後臺執行任務
- php後臺定時執行任務PHP
- Django配置celery執行非同步任務和定時任務Django非同步
- 畫江湖之 PHP 多執行緒開發 【利用多執行緒 序列任務變並行處理 從而減少序列執行時間】PHP執行緒並行
- 鴻蒙程式設計江湖:I/O 密集型任務處理及 ArkTS 的非同步鎖機制鴻蒙程式設計非同步
- 計劃任務執行批處理指令碼,執行記錄顯示“上次執行結果(0x1)”指令碼
- Spring多執行緒事務處理Spring執行緒
- 嗯,真香!使用 www-data 使用者執行定時任務(cron)
- Laravel 定時任務突然無法執行Laravel
- Laravel 7使用swoole+redis每秒處理任務LaravelRedis
- go 協程初體驗 [模擬使用者執行緒池,處理 50 個任務 jobs]Go執行緒
- ThreadPoolExecutor執行緒池任務執行失敗的時候會怎樣thread執行緒
- PHP 多程式處理任務PHP
- 驚!Laravel自帶執行定時任務的命令,只推薦本地使用Laravel
- @Async使用ThreadPoolTaskExecutor 多執行緒thread執行緒