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