返回文章列表

Rust 非同步程式設計 Tokio 執行時期深入解析

本文探討 Rust 中的非同步程式設計,特別是 Tokio 執行時期的應用。文章涵蓋 Future、async/await、tokio::task::spawn、非同步觀察者模式等關鍵概念,並以實際程式碼範例闡述同步與非同步程式設計的差異、阻塞與非阻塞操作的比較,以及平行與並發的區別,最後示範如何在非同步上下文中使用

程式語言 後端開發

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!");
}

內容解密:

  1. tokio::time::{sleep, Duration}:匯入 Tokio 的 sleepDuration,用於建立一個延遲 Future。
  2. #[tokio::main]:標記 main 函式為非同步執行入口。
  3. async fn main():定義一個非同步 main 函式。
  4. let future = sleep(Duration::from_secs(1)):建立一個延遲 1 秒的 Future。
  5. 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!");
}

程式碼解析:

  1. 同步版本:直接使用 thread::sleep 阻塞當前執行緒,簡單直接。
  2. 非同步版本:使用 tokio::time::sleep 進行非同步睡眠,並透過 .await 讓出控制權給排程器,避免阻塞主執行緒。

避免阻塞主執行緒

在非同步程式設計中,避免阻塞主執行緒是至關重要的。所謂阻塞主執行緒,指的是長時間佔用執行時期(Runtime),使得排程器無法切換任務。I/O 操作和耗時的 CPU 任務都可能導致阻塞。

如何避免阻塞?

  1. 引入讓步點(Yield Points):透過 .awaittokio::task::spawn_blocking() 將控制權交回給排程器,讓其他任務有機會執行。
  2. 區分快慢操作:瞭解不同操作的耗時特性,如函式呼叫通常很快,而 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");
}

程式碼解析:

  1. async 區塊:定義了一個非同步程式碼區塊,該區塊內的程式碼不會立即執行,直到被 await
  2. .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");
}

內容解密:

  1. my_async_function被定義為一個非同步函式,使用async fn語法。
  2. main函式中,呼叫my_async_function().await來等待非同步任務的完成。
  3. .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();
}

內容解密:

  1. not_an_async_function傳回一個JoinHandle,該handle與使用tokio::task::spawn啟動的非同步任務相關聯。
  2. 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);
    });
}

內容解密:

  1. not_an_async_function接受一個Tokio runtime的handle,並使用它來阻塞等待一個非同步任務的完成。
  2. 在新的執行緒中呼叫not_an_async_function,並傳遞runtime handle。

非同步程式設計中的平行與並發

在Rust的非同步程式設計中,需要手動管理平行與並發。Tokio提供了多種方式來實作這兩者,包括使用tokio::task::spawntokio::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"))
    );
}

內容解密:

  1. sleep_1s_blocking函式模擬一個阻塞操作,用於測試平行與並發。
  2. 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 函式實作非阻塞式睡眠,該函式是非同步的,允許其他任務在等待期間被排程執行。

平行與並發執行的測試

透過以下三個測試案例,可以觀察到平行與並發執行的差異:

  1. 順序執行:任務按順序一個接一個執行。
  2. 並發執行(相同執行緒,非阻塞):多個任務在同一個執行緒上交替執行,看起來像是同時進行。
  3. 平行執行:多個任務在不同的執行緒或處理器上同時執行。

內容解密:

  • 在使用阻塞式睡眠的情況下,只有當任務被明確地分配到不同的執行緒(如使用 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;
        })
    }
}

內容解密:

  • 定義 Observer trait,其中 observe 方法傳回一個包裝在 Box 中的 Future
  • MyObserver 的實作中,observe 方法傳回一個非同步操作的Future,利用Tokio的 sleep 函式模擬非同步行為。