Rust 語言以其獨特的特性,為非同步程式設計提供了強大的支援。本文將探討 Rust 中的非同步程式設計核心概念,並著重於 Tokio 執行時期的應用。我們將會探討 Future 的使用方式,以及如何結合 async 和 await 關鍵字來撰寫非同步程式碼。此外,還會介紹 tokio::task::spawn 的使用方法,以及如何在非同步和非同步上下文中正確使用 .await。文章也將探討如何在 Rust 中實作非同步觀察者模式,並比較阻塞式和非阻塞式操作的差異,以及平行與並發的區別。
8.3 Future:處理非同步任務結果
大多數非同步函式庫和語言都根據 Future 設計模式,用於處理傳回未來結果的任務。當執行非同步操作時,操作的結果是一個 Future,而不是直接傳回操作的值。
使用 Future 的範例
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let future = sleep(Duration::from_secs(1));
println!("Sleeping...");
future.await;
println!("Done!");
}
內容解密:
tokio::time::{sleep, Duration}:匯入 Tokio 的sleep和Duration,用於建立一個延遲 Future。#[tokio::main]:標記main函式為非同步執行入口。async fn main():定義一個非同步main函式。let future = sleep(Duration::from_secs(1)):建立一個延遲 1 秒的 Future。future.await:等待 Future 完成。
Future 提供了一種方便的抽象,但需要程式設計師正確處理。在使用 Future 時,需要注意執行的上下文和執行的順序,以確保正確的結果。
深入理解 Rust 中的非同步程式設計
在現代軟體開發中,非同步程式設計(Async Programming)已成為處理高並發任務和提高程式效能的重要手段。Rust 語言透過其強大的型別系統和所有權模型,為非同步程式設計提供了堅實的基礎。本文將探討 Rust 中的非同步程式設計,特別是使用 Tokio 執行時期(Runtime)進行非同步任務處理的相關知識。
為什麼需要非同步程式設計?
在同步程式設計中,當程式執行到一個阻塞操作(如 I/O 操作或睡眠)時,整個執行緒會被阻塞,直到該操作完成。這種方式在處理大量並發任務時會導致效能瓶頸。非同步程式設計允許程式在等待某個操作完成的同時,繼續執行其他任務,從而提高整體的並發性和回應速度。
非同步與同步的對比
首先,我們來對比一下同步和非同步程式碼的不同。以下是一個簡單的例子,分別展示瞭如何使用同步和非同步的方式讓程式睡眠 1 秒後列印 “Hello, world!"。
同步版本
fn main() {
use std::{thread, time};
let duration = time::Duration::from_secs(1);
thread::sleep(duration);
println!("Hello, world!");
}
非同步版本
#[tokio::main]
async fn main() {
use std::time;
let duration = time::Duration::from_secs(1);
tokio::time::sleep(duration).await;
println!("Hello, world!");
}
程式碼解析:
- 同步版本:直接使用
thread::sleep阻塞當前執行緒,簡單直接。 - 非同步版本:使用
tokio::time::sleep進行非同步睡眠,並透過.await讓出控制權給排程器,避免阻塞主執行緒。
避免阻塞主執行緒
在非同步程式設計中,避免阻塞主執行緒是至關重要的。所謂阻塞主執行緒,指的是長時間佔用執行時期(Runtime),使得排程器無法切換任務。I/O 操作和耗時的 CPU 任務都可能導致阻塞。
如何避免阻塞?
- 引入讓步點(Yield Points):透過
.await或tokio::task::spawn_blocking()將控制權交回給排程器,讓其他任務有機會執行。 - 區分快慢操作:瞭解不同操作的耗時特性,如函式呼叫通常很快,而 I/O 操作則相對較慢。
使用 Tokio 執行時期
Tokio 是 Rust 中最流行的非同步執行時期之一,它提供了豐富的功能和易用的 API。
定義執行時期
使用 #[tokio::main] 巨集,可以簡化非同步程式的啟動流程:
#[tokio::main]
async fn main() {
// 非同步程式碼
}
這個巨集會自動建立 Tokio 執行時期,並將 main 函式轉換為非同步函式。
async 和 .await 的使用
async 和 .await 是 Rust 非同步程式設計的核心關鍵字。
async:標記一個函式或程式碼區塊為非同步,傳回一個 Future。.await:等待一個 Future 完成,並在必要時讓出控制權給排程器。
範例:使用 async 和 .await
#[tokio::main]
async fn main() {
async {
println!("This line prints first");
}.await;
println!("This line prints second");
}
程式碼解析:
async區塊:定義了一個非同步程式碼區塊,該區塊內的程式碼不會立即執行,直到被await。.await:等待非同步區塊完成執行,確保列印順序符合預期。
使用async和.await的時機與方法
在Rust程式語言中,async和.await是處理非同步任務的核心關鍵字。正確地使用這兩個關鍵字,可以讓開發者撰寫出高效、非阻塞的程式碼。
async和.await的基本用法
async關鍵字用於定義一個非同步函式或程式區塊,而.await則用於等待非同步任務的完成。以下是一個簡單的例子:
async fn my_async_function() {
println!("This is an async function");
}
#[tokio::main]
async fn main() {
my_async_function().await;
println!("This line prints after my_async_function completes");
}
內容解密:
my_async_function被定義為一個非同步函式,使用async fn語法。- 在
main函式中,呼叫my_async_function().await來等待非同步任務的完成。 .await關鍵字使得程式在此處暫停,直到my_async_function完成。
tokio::task::spawn的使用
有時,我們需要在非同步上下文之外啟動一個非同步任務,這時可以使用tokio::task::spawn。這個函式會將任務交給Tokio runtime執行,並傳回一個JoinHandle,用於等待任務的完成。
use tokio::task::JoinHandle;
fn not_an_async_function() -> JoinHandle<()> {
tokio::task::spawn(async {
println!("Second print statement");
})
}
#[tokio::main]
async fn main() {
println!("First print statement");
not_an_async_function().await.ok();
}
內容解密:
not_an_async_function傳回一個JoinHandle,該handle與使用tokio::task::spawn啟動的非同步任務相關聯。- 在
main函式中,先列印"First print statement”,然後呼叫not_an_async_function().await.ok()來等待非同步任務的完成。
在非async上下文中使用.await
直接在非async函式中使用.await是不允許的,但可以透過Tokio runtime的handle來阻塞等待future的結果。
use tokio::runtime::Handle;
fn not_an_async_function(handle: Handle) {
handle.block_on(async {
println!("Second print statement");
})
}
#[tokio::main]
async fn main() {
println!("First print statement");
let handle = Handle::current();
std::thread::spawn(move || {
not_an_async_function(handle);
});
}
內容解密:
not_an_async_function接受一個Tokio runtime的handle,並使用它來阻塞等待一個非同步任務的完成。- 在新的執行緒中呼叫
not_an_async_function,並傳遞runtime handle。
非同步程式設計中的平行與並發
在Rust的非同步程式設計中,需要手動管理平行與並發。Tokio提供了多種方式來實作這兩者,包括使用tokio::task::spawn、tokio::join!和tokio::select!巨集。
async fn sleep_1s_blocking(task: &str) {
use std::{thread, time::Duration};
println!("Entering sleep_1s_blocking({task})");
thread::sleep(Duration::from_secs(1));
println!("Returning from sleep_1s_blocking({task})");
}
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
// 測試1:順序執行
sleep_1s_blocking("Task 1").await;
sleep_1s_blocking("Task 2").await;
// 測試2:使用tokio::join!並發執行
tokio::join!(
sleep_1s_blocking("Task 3"),
sleep_1s_blocking("Task 4")
);
// 測試3:使用tokio::spawn平行執行
tokio::join!(
tokio::spawn(sleep_1s_blocking("Task 5")),
tokio::spawn(sleep_1s_blocking("Task 6"))
);
}
內容解密:
sleep_1s_blocking函式模擬一個阻塞操作,用於測試平行與並發。- 在
main函式中,分別測試了順序執行、並發執行(使用tokio::join!)和平行執行(使用tokio::spawn)。
深入理解Rust中的非同步程式設計
非同步與平行執行的差異
在探討Rust的非同步程式設計時,瞭解平行(parallelism)與並發(concurrency)的區別至關重要。平行是指多個任務在同一時刻被不同的處理器或執行緒執行,而並發則是指多個任務在同一時間段內交替執行,給人的感覺是同時進行,但在單執行緒的情況下,真正的平行是不可能的。
阻塞式與非阻塞式睡眠函式的比較
考慮以下範例:
async fn sleep_1s_blocking(task: &str) {
println!("Entering sleep_1s_blocking({task})");
std::thread::sleep(std::time::Duration::from_secs(1));
println!("Returning from sleep_1s_blocking({task})");
}
async fn sleep_1s_nonblocking(task: &str) {
use tokio::time::{sleep, Duration};
println!("Entering sleep_1s_nonblocking({task})");
sleep(Duration::from_secs(1)).await;
println!("Returning from sleep_1s_nonblocking({task})");
}
內容解密:
sleep_1s_blocking函式使用std::thread::sleep實作阻塞式睡眠,導致執行緒暫停執行,不允許其他任務在同一個執行緒上執行。sleep_1s_nonblocking函式則利用Tokio函式庫提供的sleep函式實作非阻塞式睡眠,該函式是非同步的,允許其他任務在等待期間被排程執行。
平行與並發執行的測試
透過以下三個測試案例,可以觀察到平行與並發執行的差異:
- 順序執行:任務按順序一個接一個執行。
- 並發執行(相同執行緒,非阻塞):多個任務在同一個執行緒上交替執行,看起來像是同時進行。
- 平行執行:多個任務在不同的執行緒或處理器上同時執行。
內容解密:
- 在使用阻塞式睡眠的情況下,只有當任務被明確地分配到不同的執行緒(如使用
tokio::spawn)時,才能實作真正的平行執行。 - 當改用非阻塞式睡眠時,即使在單一執行緒上,也能實作任務的並發執行。
非同步觀察者模式的實作
Rust目前不支援直接在trait中定義非同步方法,因此實作非同步觀察者模式需要一些額外的工作。主要思路是將非同步方法的傳回型別定義為一個Future,並利用Box來處理trait物件。
範例程式碼:
pub trait Observer {
type Subject;
type Output;
fn observe(&self, subject: &Self::Subject) -> Box<dyn Future<Output = Self::Output>>;
}
struct Subject;
struct MyObserver;
impl Observer for MyObserver {
type Subject = Subject;
type Output = ();
fn observe(&self, _subject: &Self::Subject) -> Box<dyn Future<Output = Self::Output>> {
Box::new(async {
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(100)).await;
})
}
}
內容解密:
- 定義
Observertrait,其中observe方法傳回一個包裝在Box中的Future。 - 在
MyObserver的實作中,observe方法傳回一個非同步操作的Future,利用Tokio的sleep函式模擬非同步行為。