 Rust 異步篇:3 個(gè)任務(wù)從 3.015s 到 1.006s,CompletableFuture/虛擬線(xiàn)程 vs tokio 一次講透(附真實(shí)報(bào)錯(cuò)))
Java 轉(zhuǎn) Rust 異步篇3 個(gè)任務(wù)從 3.015s 到 1.006sCompletableFuture/虛擬線(xiàn)程 vs tokio 一次講透附真實(shí)報(bào)錯(cuò)一句話(huà)結(jié)論Java 處理異步有三套方案線(xiàn)程池、CompletableFuture、虛擬線(xiàn)程各有各的心智負(fù)擔(dān)Rust 只有一套async/await語(yǔ)法 tokio運(yùn)行時(shí)。核心差異在「誰(shuí)負(fù)責(zé)切換」——Java 的 CompletableFuture 靠回調(diào)鏈、虛擬線(xiàn)程靠JVM 在阻塞點(diǎn)偷偷切而 Rust 靠編譯器把a(bǔ)sync fn編成狀態(tài)機(jī).await處顯式掛起、不阻塞線(xiàn)程。本文用 3 段真實(shí)報(bào)錯(cuò)E0728 / unused Future / E0277 5 組真實(shí)耗時(shí)數(shù)據(jù)含1000 個(gè)任務(wù) 1.007s一次講透。目錄一、Java 的異步為什么這么「分裂」二、Rust 的答案一套 async/await 一個(gè) tokio三、三個(gè)必踩的坑真實(shí)報(bào)錯(cuò)四、阻塞 vs 異步真實(shí)耗時(shí) 3.015s → 1.006s五、async 的真正威力1000 個(gè)任務(wù) 1 秒六、select! 競(jìng)速 異步 channel七、三方案全景對(duì)照表八、Java 老兵的 4 條心智遷移九、互動(dòng)區(qū)十、總結(jié)一張表帶走附錄復(fù)現(xiàn)命令一、Java 的異步為什么這么「分裂」Java 老兵都懂Java 的異步是一條不斷打補(bǔ)丁的路。你手頭至少有三套方案各有各的痛方案 1線(xiàn)程池傳統(tǒng)ExecutorServicepoolExecutors.newFixedThreadPool(100);pool.submit(()-{Thread.sleep(1000);// 1 個(gè)任務(wù)占 1 個(gè)線(xiàn)程阻塞等待returndone;});痛點(diǎn)1 個(gè)任務(wù) 1 個(gè)線(xiàn)程。1000 個(gè)并發(fā)任務(wù)就是 1000 個(gè)線(xiàn)程每個(gè)默認(rèn) 1MB 棧光內(nèi)存就約 1GB還有昂貴的上下文切換。方案 2CompletableFuture顯式異步CompletableFuture.supplyAsync(()-fetchUser()).thenApply(user-fetchOrders(user)).thenApply(orders-fetchTotal(orders)).exceptionally(ex-{log.error(出錯(cuò),ex);return0;});痛點(diǎn)不阻塞線(xiàn)程了但寫(xiě)法變成了「回調(diào)鏈」多個(gè)異步步驟嵌套就是著名的「回調(diào)地獄callback hell」異常處理還要單獨(dú)寫(xiě)exceptionally心智負(fù)擔(dān)重。方案 3虛擬線(xiàn)程 Virtual ThreadsJava 21try(varexecutorExecutors.newVirtualThreadPerTaskExecutor()){executor.submit(()-{Thread.sleep(1000);// 阻塞點(diǎn)JVM 自動(dòng)切換到別的虛擬線(xiàn)程returndone;});}痛點(diǎn)解決了「回調(diào)地獄」——你還是寫(xiě)同步代碼JVM 在阻塞點(diǎn)sleep/IO偷偷幫你切換。但它是隱式的魔法你看不到切換點(diǎn)在哪調(diào)試、追蹤、理解底層行為反而變難??偨Y(jié) Java 的困境三套方案心智模型完全不同你得根據(jù)場(chǎng)景「選邊站」。二、Rust 的答案一套 async/await 一個(gè) tokioRust只有一套async/await語(yǔ)法 tokio運(yùn)行時(shí)。它把「異步」這件事用顯式的方式統(tǒng)一了。核心就三件事async fn返回一個(gè)Future這個(gè) Future 是「一個(gè)未來(lái)會(huì)算出的值」。.await是掛起點(diǎn)執(zhí)行到.await時(shí)如果還沒(méi)就緒當(dāng)前任務(wù)掛起、讓出線(xiàn)程線(xiàn)程可以去干別的任務(wù)。不阻塞 OS 線(xiàn)程。編譯器把a(bǔ)sync fn編譯成「狀態(tài)機(jī)」每個(gè).await就是一個(gè)狀態(tài)掛起時(shí)記錄「我停在第幾個(gè) await」恢復(fù)時(shí)從那里接著跑。對(duì)比三者的「誰(shuí)負(fù)責(zé)切換」方案誰(shuí)負(fù)責(zé)切換切換點(diǎn)可見(jiàn)嗎CompletableFuture你寫(xiě)回調(diào)鏈顯式但很啰嗦Virtual ThreadsJVM隱式阻塞點(diǎn)自動(dòng)切你看不到Rust async/await編譯器 tokio 運(yùn)行時(shí)顯式.await就在你眼前一個(gè)精妙的類(lèi)比虛擬線(xiàn)程是「寫(xiě)同步代碼偷偷異步」Rust 的 async 是「寫(xiě)異步代碼但語(yǔ)法糖讓它長(zhǎng)得像同步」。兩者殊途同歸——都是「別讓 OS 線(xiàn)程傻等 IO」但 Rust 把切換點(diǎn)明明白白寫(xiě)在了代碼里那個(gè).await而不是藏在 JVM 里。三、三個(gè)必踩的坑真實(shí)報(bào)錯(cuò)Rust 的 async 對(duì)新手有三個(gè)幾乎人人必踩的坑。我們一個(gè)個(gè)用真實(shí)報(bào)錯(cuò)看清楚???1.await只能在 async 里用E0728你在普通main里想調(diào)用異步函數(shù)直覺(jué)是直接.awaitasyncfnfoo()-i32{42}fnmain(){letxfoo().await;// ? await 只能在 async 函數(shù)或 async 塊里println!({},x);}真實(shí)報(bào)錯(cuò)cargo build --bin a1_await_outsideerror[E0728]: await is only allowed inside async functions and blocks -- src/bin/a1_await_outside.rs:5:19 | 4 | fn main() { | --------- this is not async 5 | let x foo().await; | ^^^^^ only allowed inside async functions and blocks為什么.await會(huì)「掛起」而掛起只能發(fā)生在「異步上下文」里。普通函數(shù)一旦返回就沒(méi)了沒(méi)有「掛起再恢復(fù)」的能力。解法是加#[tokio::main]把main變成 async#[tokio::main]// 把 main 包進(jìn) tokio 運(yùn)行時(shí)讓它能 awaitasyncfnmain(){letxfoo().await;// ? 現(xiàn)在可以了println!({},x);}坑 2Future 沒(méi) await 就什么都不做編譯警告這是最隱蔽、坑人最多的一個(gè)。你以為調(diào)用了async fn就是「執(zhí)行了」asyncfnfoo()-i32{println!(這條不會(huì)打印因?yàn)?Future 沒(méi)被 await);42}#[tokio::main]asyncfnmain(){foo();// ?? 只是創(chuàng)建了 Future根本沒(méi)有執(zhí)行它println!(main 結(jié)束);}真實(shí)警告cargo build --bin a2_future_not_awaitedwarning: unused implementer of Future that must be used -- src/bin/a2_future_not_awaited.rs:9:5 | 9 | foo(); | ^^^^^ | note: futures do nothing unless you .await or poll them note: #[warn(unused_must_use)] on by default關(guān)鍵foo()不執(zhí)行函數(shù)體它只是構(gòu)造了一個(gè) Future 對(duì)象等你.await它才真正開(kāi)跑。運(yùn)行結(jié)果就是這條不會(huì)打印永遠(yuǎn)不會(huì)出現(xiàn)只打印main 結(jié)束。這和 Java 完全不同。Java 里CompletableFuture.supplyAsync(() - ...)一調(diào)用就開(kāi)始執(zhí)行了Rust 里foo()只是「造了個(gè)計(jì)劃」不.await就永遠(yuǎn)不執(zhí)行。Future 是惰性的lazy。Rust 特意給Future加了#[must_use]就是為了讓你注意到「這個(gè)值你必須用」。坑 3tokio::spawn要求 Future 是SendE0277想把任務(wù)扔到另一個(gè)線(xiàn)程跑用tokio::spawn。但如果任務(wù)捕獲了Rc非線(xiàn)程安全編譯器會(huì)攔usestd::rc::Rc;#[tokio::main]asyncfnmain(){letrcRc::new(5);tokio::spawn(asyncmove{println!({},rc);// ? Rci32 不是 Send不能跨線(xiàn)程}).await.unwrap();}真實(shí)報(bào)錯(cuò)cargo build --bin a3_rc_not_senderror: future cannot be sent between threads safely -- src/bin/a3_rc_not_send.rs:7:18 | 7 | tokio::spawn(async move { | __________________^ 8 | | println!({}, rc); 9 | | }).await.unwrap(); | |_____^ future created by async block is not Send | help: within {async block...}, the trait Send is not implemented for Rci32 note: required by a bound in tokio::spawn | | pub fn spawnF(future: F) - JoinHandleF::Output | where | F: Future Send static, | ^^^^ required by this bound in spawn這正是第6篇Send/Sync在異步世界的延續(xù)tokio::spawn的簽名要求F: Future Send static而Rci32不是Send于是整個(gè) Future 就不是Send編譯失敗。修復(fù)跨線(xiàn)程用Arc原子計(jì)數(shù)letrcArc::new(5);// 換成 Arc 即可tokio::spawn(asyncmove{println!({},rc);}).await.unwrap();四、阻塞 vs 異步真實(shí)耗時(shí) 3.015s → 1.006s先看一個(gè) Java 老兵最容易犯的錯(cuò)在 async 里用了阻塞的std::thread::sleep。反例a4_blocking——3 個(gè)「睡 1 秒」的任務(wù)用join!一起跑#[tokio::main]asyncfnmain(){letstartInstant::now();tokio::join!(async{std::thread::sleep(Duration::from_secs(1));println!(任務(wù)1 完成);},async{std::thread::sleep(Duration::from_secs(1));println!(任務(wù)2 完成);},async{std::thread::sleep(Duration::from_secs(1));println!(任務(wù)3 完成);},);println!(阻塞版耗時(shí): {:?},start.elapsed());}真實(shí)運(yùn)行輸出任務(wù)1 完成 任務(wù)2 完成 任務(wù)3 完成 阻塞版耗時(shí): 3.015182875s為什么是 3 秒std::thread::sleep是同步阻塞它把整個(gè) OS 線(xiàn)程卡住了。即使join!想并發(fā)任務(wù)也一個(gè)接一個(gè)串行跑完3 個(gè) × 1s 3s。正例a5_concurrent——換成異步的tokio::time::sleep#[tokio::main]asyncfnmain(){letstartInstant::now();tokio::join!(async{tokio::time::sleep(Duration::from_secs(1)).await;println!(任務(wù)1 完成);},async{tokio::time::sleep(Duration::from_secs(1)).await;println!(任務(wù)2 完成);},async{tokio::time::sleep(Duration::from_secs(1)).await;println!(任務(wù)3 完成);},);println!(并發(fā)版耗時(shí): {:?},start.elapsed());}真實(shí)運(yùn)行輸出任務(wù)2 完成 任務(wù)3 完成 任務(wù)1 完成 并發(fā)版耗時(shí): 1.006284084s兩個(gè)關(guān)鍵證據(jù)耗時(shí) 3.015s → 1.006s3 個(gè) 1 秒任務(wù)并發(fā)跑總耗時(shí)約 1 秒加速 3 倍。完成順序亂序任務(wù)2 → 任務(wù)3 → 任務(wù)1這證明了它們真的在并發(fā)而不是串行。核心差異一句話(huà)std::thread::sleep阻塞 OS 線(xiàn)程tokio::time::sleep掛起任務(wù)、讓出線(xiàn)程。在 async 里一切阻塞操作都要換成 tokio 的異步版本tokio::time::sleep、tokio::fs::read、tokio::net……否則一個(gè)阻塞調(diào)用就會(huì)拖垮整個(gè)運(yùn)行時(shí)。五、async 的真正威力1000 個(gè)任務(wù) 1 秒前面的 3 倍加速只是開(kāi)胃菜。async 真正的威力是——1000 個(gè)并發(fā)任務(wù)一個(gè)或幾個(gè)線(xiàn)程就夠??碼8_many_tasks#[tokio::main]asyncfnmain(){letstartInstant::now();letmuttasksvec![];foriin0..1000{tasks.push(tokio::spawn(asyncmove{tokio::time::sleep(Duration::from_secs(1)).await;i}));}letmutsum0;fortintasks{sumt.await.unwrap();}println!(1000 個(gè)任務(wù)總耗時(shí): {:?},start.elapsed());println!(求和校驗(yàn)(0..1000): {},sum);// 應(yīng)為 499500}真實(shí)運(yùn)行輸出1000 個(gè)任務(wù)總耗時(shí): 1.007533042s 求和校驗(yàn)(0..1000): 499500這組數(shù)據(jù)的震撼點(diǎn)1000 個(gè)「睡 1 秒」的任務(wù)總耗時(shí)仍是約 1 秒不是 1000 秒求和校驗(yàn) 499500 01…999說(shuō)明1000 個(gè)任務(wù)全部正確完成、一個(gè)不漏而這 1000 個(gè)任務(wù)只跑在幾個(gè) OS 線(xiàn)程上tokio 多線(xiàn)程運(yùn)行時(shí)默認(rèn) CPU 核心數(shù)個(gè)線(xiàn)程每個(gè)任務(wù)掛起時(shí)幾乎不占棧內(nèi)存。對(duì)照 Java 的傳統(tǒng)線(xiàn)程池1000 個(gè)線(xiàn)程 × 1MB 棧 ≈ 1GB 內(nèi)存。而 Rust async 里 1000 個(gè)掛起任務(wù)內(nèi)存占用是幾 MB 級(jí)別。配圖直觀感受一下誠(chéng)實(shí)說(shuō)明Java 21 的虛擬線(xiàn)程也能高效跑 1000 個(gè)任務(wù)這正是它存在的意義所以「1000 線(xiàn)程」只針對(duì)傳統(tǒng)線(xiàn)程池。但虛擬線(xiàn)程的切換是 JVM 在阻塞點(diǎn)隱式做的你看不到Rust 的.await是顯式寫(xiě)在代碼里的。這是寫(xiě)法哲學(xué)上的差異不是「誰(shuí)更快」的簡(jiǎn)單高下。六、select! 競(jìng)速 異步 channel再補(bǔ)兩個(gè) tokio 的實(shí)用工具直接對(duì)標(biāo) Java 的CompletableFuture.anyOf和BlockingQueue。select!多個(gè) Future 誰(shuí)先完成就用誰(shuí)對(duì)標(biāo) Java 的CompletableFuture.anyOf(...)#[tokio::main]asyncfnmain(){tokio::select!{_tokio::time::sleep(Duration::from_secs(2)){println!(慢任務(wù)2s完成);}_tokio::time::sleep(Duration::from_secs(1)){println!(快任務(wù)1s完成select 提前返回);}}}真實(shí)運(yùn)行輸出快任務(wù)1s完成select 提前返回select!同時(shí)「監(jiān)聽(tīng)」多個(gè) Future誰(shuí)先完成就執(zhí)行誰(shuí)的分支其余 Future 自動(dòng)被取消。這是實(shí)現(xiàn)超時(shí)、競(jìng)速、優(yōu)雅關(guān)閉的利器。異步 channel生產(chǎn)者和消費(fèi)者都是.await對(duì)標(biāo) Java 的BlockingQueue但send/recv都是異步的#[tokio::main]asyncfnmain(){let(tx,mutrx)tokio::sync::mpsc::channel(10);tokio::spawn(asyncmove{foriin1..3{tx.send(i).await.unwrap();// 異步發(fā)送}});whileletSome(v)rx.recv().await{// 異步接收println!(收到: {},v);}}真實(shí)運(yùn)行輸出收到: 1 收到: 2 收到: 3和第6篇的std::sync::mpsc是同一個(gè)「多生產(chǎn)者單消費(fèi)者」思想但異步版send滿(mǎn)時(shí)會(huì).await掛起而不是阻塞recv空時(shí)也掛起。在 async 世界通信也要用異步原語(yǔ)。七、三方案全景對(duì)照表把 Java 的三套方案和 Rust 的 async 放在一張表里維度CompletableFutureVirtual ThreadsRust async/await tokio寫(xiě)法顯式回調(diào)鏈同步代碼顯式async/.await誰(shuí)來(lái)切換你thenApply 鏈JVM阻塞點(diǎn)隱式切編譯器狀態(tài)機(jī) tokio切換點(diǎn)可見(jiàn)性可見(jiàn)但啰嗦不可見(jiàn)黑魔法可見(jiàn).await就在眼前不阻塞線(xiàn)程是是是1000 并發(fā)任務(wù)能線(xiàn)程池能能1 秒實(shí)測(cè)學(xué)習(xí)曲線(xiàn)陡組合子多緩寫(xiě)同步中語(yǔ)法糖類(lèi)型安全弱泛型 Future—強(qiáng)Send/static編譯期查生態(tài)碎片化新興tokio 一家獨(dú)大一句話(huà)回調(diào)地獄隱式魔法顯式狀態(tài)機(jī)八、Java 老兵的 4 條心智遷移1. 「調(diào)用異步函數(shù)」≠「執(zhí)行異步函數(shù)」。Java 里supplyAsync(...)一調(diào)就開(kāi)跑Rust 里foo()只是造了個(gè)惰性 Future不.await就是白寫(xiě)。這是坑 2 的核心。2. 阻塞和掛起是兩個(gè)世界。std::thread::sleep阻塞線(xiàn)程3.015s 的教訓(xùn)tokio::time::sleep掛起任務(wù)1.006s 的結(jié)果。進(jìn)了 async所有阻塞調(diào)用都要換成 tokio 的異步版。3..await是「協(xié)作式」切換點(diǎn)。Java 虛擬線(xiàn)程是「搶占式隱式切換」你看不到Rust 的.await是「協(xié)作式顯式讓出」——只有寫(xiě).await的地方才會(huì)讓出線(xiàn)程。好處是切換點(diǎn)可控、可讀壞處是你得記得寫(xiě).await。4.Send/Sync依然在守門(mén)。異步只是換了種「并發(fā)」形式線(xiàn)程安全的規(guī)則沒(méi)變跨線(xiàn)程spawn的任務(wù)必須SendRc得換Arc。第6篇的知識(shí)在 async 里原封不動(dòng)地復(fù)用。九、互動(dòng)區(qū)投票你最常用或最想用哪種異步方案?jìng)鹘y(tǒng)線(xiàn)程池 Thread簡(jiǎn)單直接就是費(fèi)資源CompletableFuture顯式異步就是回調(diào)鏈難受??Virtual ThreadsJava 21 新歡寫(xiě)同步代碼Rust async/await tokio就是奔著它來(lái)的評(píng)論區(qū)聊聊你在 Java 里被 CompletableFuture 的thenApply鏈折磨過(guò)嗎最深嵌套了幾層對(duì)于「隱式切換虛擬線(xiàn)程vs 顯式.awaitRust」你更喜歡哪種為什么如果這篇幫你把 async/await 和 tokio 的「為什么」想通了點(diǎn)個(gè)贊 收藏讓更多 Java 老兵少在回調(diào)地獄里打轉(zhuǎn)。十、總結(jié)一張表帶走維度JavaRust異步方案三套線(xiàn)程池 / CompletableFuture / 虛擬線(xiàn)程一套async/await tokio核心機(jī)制回調(diào)鏈 / JVM 隱式切換編譯器生成狀態(tài)機(jī) 事件循環(huán)切換點(diǎn)隱式或啰嗦顯式.awaitFuture 語(yǔ)義一調(diào)用就執(zhí)行惰性不 await 不執(zhí)行1000 并發(fā)任務(wù)傳統(tǒng)線(xiàn)程池 ~1GB 內(nèi)存幾 MB1.007s 實(shí)測(cè)線(xiàn)程安全靠自覺(jué)Send/Sync編譯期把關(guān)一句話(huà)三套方案三套心智一套方案.await講透Rust 的異步哲學(xué)不給你三套方案讓你糾結(jié)而是一套async/await用「編譯器生成狀態(tài)機(jī)」「顯式.await」「Send類(lèi)型檢查」三件事把異步變得可讀、可預(yù)測(cè)、類(lèi)型安全。1000 個(gè)任務(wù) 1 秒就是這套設(shè)計(jì)的實(shí)力證明。附錄復(fù)現(xiàn)命令# 所有示例在 async-demo/ 目錄依賴(lài) tokio 1.53.2cdasync-demo# 三條真實(shí)報(bào)錯(cuò)/警告cargobuild--bina1_await_outside# E0728: await 只能在 async 里cargobuild--bina2_future_not_awaited# warning: Future 沒(méi) await 什么都不做cargobuild--bina3_rc_not_send# error: Rc 不是 Send# 五組真實(shí)運(yùn)行輸出cargorun--bina4_blocking# 阻塞版 3.015scargorun--bina5_concurrent# 并發(fā)版 1.006s完成順序亂序cargorun--bina6_select# 快任務(wù)先完成cargorun--bina7_channel# 收到 1/2/3cargorun--bina8_many_tasks# 1000 任務(wù) 1.007s求和 499500# 配圖腳本可選python3../scripts/make_async_diagram.py