2024-01-28|閱讀時間 ‧ 約 30 分鐘

在 Rust 裡使用非同步(Asynchronous)

非同步程式設計(Asynchronous programming) 或是簡單的稱之為 async,它是一種並發程式模型(concurrent programming model),其目的就是讓多個任務能同時在作業系統的執行緒上執行,並透過 async/.await 保留同步。

Sync 與 Async 簡單的分別:

  • 同步(Sync):做完一件事情後再做下一件
  • 非同步(Async):一件事情還沒完成時,可以先做其他不衝突的事情
    • 並發、並行(Concurrency):在同個程式下,各個任務可獨立執行平行(Parallelism):同時執行多個程式
    • 平行(Parallelism):同時執行多個程式

Async 與其他並發程式模型

根據不同程式語言所支持的並發程式模型,以下列出幾種常見的幾種:

  • 作業系統執行緒(OS threads):不需要改變任何程式模型,就可以簡單的達成並發,但是,各個執行緒之間要同步就很困難,而且需要更多的效能,不夠應付大量密集的 IO 工作
  • 事件驅動程式設計(Event-driven programming):最相關的關鍵詞就是 callback,高效率但語法很容易過冗,而且很難追蹤資料流與錯誤的問題
  • 協程(Coroutines):與 threads 一樣不需要改變任何程式模型,就可以簡單的達成並發。與 async 一樣可支援大量任務,不過它抽象化了許多系統程式設計與自訂運行的低階細節
  • 演員模型(Actor Model):將所有並發計算切成 actor 的單元,並透過 message 溝通,很像分散式系統。不過有很多問題待解決,例如:流量控制、邏輯問題

與上述做比較,在 Rust 等低階程式語言裡,使用非同步程式設計可以實現高性能並同時提供 threads 與 coroutines 的各式優點。

在 Rust 裡的 Async 與 threads

如果在 Rust 使用 OS threads,可以使用 std::threads 或間接訪問 threads pool。

OS threads

OS threads 適合少量任務,執行緒會使用到 CPU 與記憶體,生成 threads 和在 threads 之間切換是很耗效能的,連空閒的 threads 也會消耗效能,雖然使用 threads pool 可以減輕一些使用成本,但也不是全部。

Threads 能讓你重複使用已存在的同步程式碼,而不需要大幅的改變程式,也不需要特定的設計模型,在一些特定的 OS 中,你還可以設定 threads 的優先級來決定誰先執行。

Async

Async 明顯的減少 CPU 與記憶體的消耗,特別是對於具有大量密集的 IO 工作,例如:伺服器和資料庫。在同等的條件下,使用 async 可以處理比 OS threads 更多的任務,這是因為 async 使用了少量的 threads 來處理大量的簡單任務。

最後要註明一點,並不是 async 比 threads 好,而是要根據當下的狀況來選擇使用,如果你不需要 async 的功能,直接使用 threads 反而是更簡單的方式。

實例比較

建立兩個 threads 來同時下載兩個網頁:

fn get_two_sites() {
// 產生兩個 threads
let thread_one = thread::spawn(|| download("https://www.foo.com"));
let thread_two = thread::spawn(|| download("https://www.bar.com"));

// 等待 threads 完成工作
thread_one.join().expect("thread one panicked");
thread_two.join().expect("thread two panicked");
}

不過這種簡單的任務建立兩個 threads 是很浪費的,在大型的應用程式中每一點資源都是很重要的,所以在這裡,我們可以選擇 Rust 中的 async 方式,既不需要使用額外的執行緒,還可以達成並發的功能:

async fn get_two_sites_async() {
// 建立兩個不同的 futures,當運行程式會使用 async 的方式來下載
let future_one = download_async("https://www.foo.com");
let future_two = download_async("https://www.bar.com");

// 同時執行兩個 futures
join!(future_one, future_two);
}

這樣寫的話就不會產生額外的 threads,當然唯一的重點就是要自己完成 download_async 的非同步程式碼。

async/.await

async/.await 是 Rust 內建將非同步編寫成像同步程式碼的方式,async 會將這個指定的區塊轉成一個狀態機制(state machine),稱為 Future

Future 是 Rust 裡非同步(Async) 任務的狀態:

  • poll:不斷查詢任務是否完成
    • Pending:任務還在執行Ready(val):如果成功完成任務,則連同結果一起回傳
    • Ready(val):如果成功完成任務,則連同結果一起回傳

實際操作

這裡使用教學裡的 futures 來實現非同步程式碼,首先新增依賴項到 Cargo.toml

[dependencies]
futures = "0.3"

接著在更改 src/main.rs

// `block_on` 會阻擋目前的 thread 直到 future 完成
use futures::executor::block_on;

async fn hello_world() {
println!("hello, world!");
}

fn main() {
let future = hello_world();
block_on(future);
}

這邊來更改使用 async/.await 來寫:

use futures::executor::block_on;
use std::{thread, time::Duration};

async fn read_book() {
// 設定一個等待時間
thread::sleep(Duration::from_secs(1));
}

async fn tell_story() {
// 等待完成後才會繼續
read_book().await;
println!("Book content");
}

async fn hand_move() {
println!("Hand move");
}

async fn async_main() {
let f1 = tell_story();
let f2 = hand_move();

// `join!` 類似於 `.await` 可以一次等待多個 futures
futures::join!(f1, f2);
}

fn main() {
block_on(async_main());
}

根據以上的例子,要先讀一本書的內容才能開始說故事,在說故事的期間同時還會有一些肢體動作,如果沒有先讀那本書,後面的動作則無法成立。

結語

以上是一個簡單的 Rust 實作 Async 的方式,在依賴項的部分,你也可以使用更多人在用的 tokio 來實現非同步。

Reference

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.