Rust 的非同步程式設計模型建立在 Future 之上,它代表一個可能在未來完成的運算。Tokio 作為一個非同步執行時,負責排程和執行這些 FutureFuture 的核心是 poll 方法,Tokio 執行器會不斷輪詢 Future 的狀態。當 Future 尚未完成時,它會傳回 Poll::Pending,並透過 Waker 機制通知執行器在適當的時機再次輪詢。這個機制允許多個 Future 在單個執行緒上併發執行,有效提升 I/O 密集型應用程式的效能。理解 Futurepoll 方法和 Waker 的互動方式對於掌握 Rust 非同步程式設計至關重要。

深入理解 Rust 的非同步程式設計

在前面的章節中,我們已經看到了如何在 Rust 中編寫同步、多執行緒和非同步程式的簡單範例。現在,讓我們更深入地探討 Rust 中的非同步程式設計。

非同步程式設計的原理

非同步程式設計允許我們在單一作業系統執行緒上同時處理多個任務。但這是如何實作的呢?畢竟,CPU 一次只能處理一組指令。

秘訣在於利用程式執行中的某些情況,即 CPU 正在等待某些外部事件或動作完成。例如,等待讀取或寫入檔案到磁碟、等待位元組到達網路連線,或等待計時器完成(如前面的範例所示)。當一段程式碼或函式正在等待磁碟子系統或網路通訊端的資料時,非同步執行環境(如 Tokio)可以在處理器上排程其他非同步任務繼續執行。當系統中斷從磁碟或 I/O 子系統到達時,非同步執行環境會識別這一點並排程原始任務繼續處理。

非同步程式設計的適用場景

一般來說,受 I/O 限制的程式(即進度速率通常取決於 I/O 子系統的速度)可能是非同步任務執行的良好候選者,而不是受 CPU 限制的程式(即進度速率取決於 CPU 的速度,例如複雜的數字運算)。這是一個廣泛而一般的準則,總是有例外。

由於 Web 開發涉及大量網路、檔案和資料函式庫 I/O,因此如果正確實施非同步程式設計,可以加快整體程式執行速度並改善終端使用者的回應時間。想像一下,您的 Web 伺服器需要處理 10,000 個或更多平行連線。使用多執行緒為每個連線產生一個單獨的作業系統執行緒,從系統資源消耗的角度來看將是非常昂貴的。早期的 Web 伺服器使用了這種模型,但後來遇到了 Web 規模系統的限制。這就是 Actix Web 框架(以及許多其他 Rust 框架)內建非同步執行環境的原因。事實上,Actix Web 在底層使用 Tokio 函式庫進行非同步任務執行(經過一些修改和增強)。

Rust 中的非同步原語

async.await 關鍵字代表了 Rust 標準函式庫中用於非同步程式設計的核心內建原語。它們只是 Rust 語法中的特殊部分,使 Rust 開發人員更容易編寫看起來像同步程式碼的非同步程式碼。

然而,Rust 非同步的核心是一個稱為 Futures 的概念。Futures 是由非同步計算(或函式)產生的單一最終值。Futures 基本上代表了延遲計算。Rust 中的非同步函式傳回一個 Future。

與 JavaScript 中的 Promise 的類別比

在 JavaScript 中,與 Rust Future 相當的概念是 Promise。當在瀏覽器中執行 JavaScript 程式碼時,當使用者發出擷取 URL 或載入影像的請求時,它不會阻塞目前的執行緒。使用者可以繼續與網頁互動。這是透過 JavaScript 引擎使用非同步處理網路擷取請求來實作的。

請注意,Rust Future 是一個比 JavaScript Promise 更低階的概念。Rust Future 是可以被輪詢是否準備就緒的東西,而 JavaScript Promise 具有更高的語義(例如,Promise 可以被拒絕)。然而,在本討論的背景下,這是一個有用的類別比。

重寫範例以展示 Futures 的使用

讓我們重寫前面的範例以展示 Futures 的使用:

use std::thread::sleep;
use std::time::Duration;
use std::future::Future;

#[tokio::main]
async fn main() {
    println!("Hello before reading file!");
    let h1 = tokio::spawn(async {
        let file1_contents = read_from_file1().await;
        println!("{:?}", file1_contents);
    });
    let h2 = tokio::spawn(async {
        let file2_contents = read_from_file2().await;
        println!("{:?}", file2_contents);
    });
    let _ = tokio::join!(h1, h2);
}

// 模擬從檔案讀取的函式
fn read_from_file1() -> impl Future<Output = String> {
    async move {
        sleep(Duration::from_secs(4));
        println!("{:?}", "Processing file 1");
        String::from("Hello, there from file 1")
    }
}

// 模擬從檔案讀取的函式
fn read_from_file2() -> impl Future<Output = String> {
    async move {
        sleep(Duration::from_secs(3));
        println!("{:?}", "Processing file 2");
        String::from("Hello, there from file 2")
    }
}

程式碼解密:

  1. read_from_file1read_from_file2 函式:這兩個函式現在傳回一個實作 Future 特徵的值。它們使用 async move 區塊來定義非同步操作。
  2. async move 區塊:這裡定義了非同步執行的程式碼。sleep 函式模擬了 I/O 等待,println! 用於輸出處理檔案的訊息。
  3. tokio::spawntokio::join!tokio::spawn 用於產生非同步任務,而 tokio::join! 用於等待多個任務完成。

這個範例展示瞭如何在 Rust 中使用 Futures 和非同步程式設計來提高程式的平行性和效率。透過使用非同步程式設計,我們可以有效地處理多個任務,而不會阻塞目前的執行緒,從而提高整體程式的效能和回應速度。

深入理解Rust非同步程式設計中的Future

在Rust的非同步程式設計中,Future是一個核心概念。它代表了一種非同步運算,能夠在未來某個時間點傳回一個值。本文將探討Future的內部工作原理,並透過具體範例來展示如何實作一個自定義的Future

Future特性解析

首先,讓我們來看看Future特性的定義:

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

這裡有兩個關鍵組成部分:

  1. Output型別:表示當Future成功完成時傳回的資料型別。
  2. poll方法:由非同步執行環境呼叫,用於檢查非同步運算是否已經完成。

poll方法的傳回值是Poll<Self::Output>,它是一個列舉型別,可以是兩種狀態之一:

  • Poll::Pending:表示Future尚未準備好。
  • Poll::Ready(val):表示Future已經完成,並傳回了值val

實作自定義的Future

為了更好地理解Future的工作原理,我們來實作一個自定義的Future。首先,定義一個名為ReadFileFuture的結構體:

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::thread::sleep;
use std::time::Duration;

struct ReadFileFuture {}

impl Future for ReadFileFuture {
    type Output = String;

    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        println!("Tokio! Stop polling me");
        Poll::Pending
    }
}

程式碼解析:

  • 我們定義了一個名為ReadFileFuture的空結構體,並為它實作了Future特性。
  • poll方法中,我們簡單地列印了一條訊息,並傳回了Poll::Pending,表示這個Future永遠不會完成。

接下來,在主函式中使用這個自定義的Future

#[tokio::main]
async fn main() {
    println!("Hello before reading file!");
    let h1 = tokio::spawn(async {
        let future1 = ReadFileFuture {};
        future1.await
    });

    let h2 = tokio::spawn(async {
        let file2_contents = read_from_file2().await;
        println!("{:?}", file2_contents);
    });
    let _ = tokio::join!(h1, h2);
}

// 模擬從檔案讀取的函式
fn read_from_file2() -> impl Future<Output = String> {
    async {
        sleep(Duration::new(2, 0));
        println!("{:?}", "Processing file 2");
        String::from("Hello, there from file 2")
    }
}

程式碼解析:

  • 在主函式中,我們產生了兩個非同步任務:h1h2
  • h1等待我們的自定義Future(即ReadFileFuture),而h2則等待另一個模擬從檔案讀取的非同步函式完成。
  • 使用tokio::join!宏同時等待兩個任務完成。

Pin和記憶體固定

在實作自定義的Future``時,我們使用了 Pin``。這是因為在Rust中,非同步運算需要被固定在記憶體中的特定位置,以確保安全性。這是Rust非同步程式設計中的一個進階概念,對於編寫正確且高效的非同步程式碼至關重要。

圖表說明:

此圖示展示了Tokio執行環境與非同步任務之間的關係,以及如何透過 poll``方法驅動 Future``的完成。

圖表翻譯: 此圖表呈現了Tokio執行環境如何管理與排程非同步任務。非同步任務中包含了 Future``,而 Future會根據其狀態傳回不同的 `Poll值。如果傳回 Pending``,表示 Future尚未準備好,通常是在等待某些外部事件。如果傳回 `Ready,則表示 `Future``已經完成並傳回了相應的值。

Rust 非同步程式設計與 Tokio 執行器深入解析

在 Rust 的非同步程式設計中,Future 是一個核心概念,表示一個可能在未來某個時間點傳回值的非同步運算。本文將探討 Future 的運作原理,以及 Tokio 執行器如何管理這些非同步任務。

自定義 Future 與 Tokio 執行器

首先,我們來看看如何自定義一個 Future。以下是一個簡單的例子,展示了一個名為 ReadFileFuture 的結構體,它實作了 Future 特徵:

impl Future for ReadFileFuture {
    type Output = String;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        println!("Tokio! 請停止輪詢我");
        cx.waker().wake_by_ref();
        Poll::Pending
    }
}

內容解密:

  1. Future 特徵實作:我們為 ReadFileFuture 實作了 Future 特徵,這使得它可以被 Tokio 執行器輪詢。
  2. poll 方法poll 方法是 Future 特徵的核心,它決定了非同步任務的狀態。在這個例子中,我們無條件傳回 Poll::Pending,表示任務尚未完成。
  3. cx.waker().wake_by_ref():這行程式碼通知 Tokio 執行器再次輪詢這個任務,實作了持續執行的效果。

Tokio 執行器的工作原理

Tokio 執行器是 Tokio 執行時的關鍵元件,負責驅動 Future 直到它們完成。以下是 Tokio 執行器與 Future 之間的互動流程:

  1. 任務建立:主函式在 Tokio 執行時上產生非同步任務。
  2. 輪詢 Future:Tokio 執行器輪詢 Future 以檢查是否準備好產生值。
  3. Waker 元件:當 Future 未準備好時,它會向 Waker 元件註冊,並在準備好時透過 wake() 方法通知執行器。
  4. 持續執行:透過 wake_by_ref() 方法,Future 可以通知執行器再次輪詢,從而實作持續執行。

圖表解析:Tokio 元件互動流程

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Rust 非同步程式設計與 Tokio 深入解析

package "Rust 記憶體管理" {
    package "所有權系統" {
        component [Owner] as owner
        component [Borrower &T] as borrow
        component [Mutable &mut T] as mutborrow
    }

    package "生命週期" {
        component [Lifetime 'a] as lifetime
        component [Static 'static] as static_lt
    }

    package "智慧指標" {
        component [Box<T>] as box
        component [Rc<T>] as rc
        component [Arc<T>] as arc
        component [RefCell<T>] as refcell
    }
}

package "記憶體區域" {
    component [Stack] as stack
    component [Heap] as heap
}

owner --> borrow : 不可變借用
owner --> mutborrow : 可變借用
owner --> lifetime : 生命週期標註
box --> heap : 堆積分配
rc --> heap : 引用計數
arc --> heap : 原子引用計數
stack --> owner : 棧上分配

note right of owner
  每個值只有一個所有者
  所有者離開作用域時值被釋放
end note

@enduml

圖表翻譯: 此圖示展示了 Tokio 的核心元件及其互動關係。Tokio Runtime 包含 Executor 和 Reactor 兩個主要元件。Executor 負責驅動 Future,而 Reactor 則監聽來自 OS Kernel 的事件。當 Future 準備好時,它透過 Waker 通知 Executor 繼續執行。

重點回顧

  • Future 是非同步運算的核心表示
  • Tokio 執行器負責驅動 Future`
  • Waker 元件用於通知執行器何時再次輪詢 Future
  • Tokio 的元件互動使得高效的非同步處理成為可能

透過本文的解析,讀者應能對 Rust 的非同步程式設計和 Tokio 有更深入的理解,並能在實際開發中應用這些知識。