Rust: Rust 異步入門 (作者洋芋,來自Rust語言中文社區)

【Rust每週一知】Rust 異步入門
原創 洋芋 Rust語言中文社區 前天

這是一篇博文翻譯,略有刪減,整理代碼方便統一閱讀,Github鏈接:https://github.com/lesterli/rust-practice/tree/master/head-first/async-primer。

原文在2月11號的【Rust日報】中給大家推薦過, 原文鏈接: https://omarabid.com/async-rust

本文並不全面介紹Rust異步主題。如果對新的async/await關鍵字Futures感到疑惑,並且對Tokio的用途很感興趣,那麼到最後應該會不再毫無頭緒。

Rust異步技術是Rust領域的新熱點,它被譽爲Rust的重要里程碑,特別適合開發高性能網絡應用程序的人們。

讓我們從頭開始。
什麼是異步?

關於Async,我給一個簡短的版本:如果有一個處理器,想同時執行(類似)兩項任務,將如何做?解決方案是先運行第一個任務,然後切換並運行第二個任務,然後再切換回去,依此類推,直到完成兩個任務。

如果想給人以計算機同時運行兩個任務的感覺(即多任務處理),則此功能很有用。另一個用例是IO操作。當程序等待網絡響應時,CPU處於空閒狀態。這是切換到另一個任務的理想時間。

那麼我們如何編寫異步代碼?

首先,讓我們從一些同步代碼開始。
同步代碼

讓我們做一個簡單的程序,該程序讀取兩個文件:file1.txt和file2.txt。我們從file1.txt開始,然後移至file2.txt。

我們將程序分爲兩個文件:main.rs和file.rs。file.rs有一個函數:read_file,在main.rs中,用每個文件的路徑爲參數調用此函數。參見下面代碼:

// sync-example/src/file.rs

use std::fs::File;
use std::io::{self, Read};

pub fn read_file(path: &str) -> io::Result<String> {
    let mut file = File::open(path)?;
    let mut buffer = String::new();
    file.read_to_string(&mut buffer)?;
    Ok(buffer)
}

// sync-example/src/main.rs

use std::io;

mod file;

fn main() -> io::Result<()> {
    println!("program started");

    let file1 = file::read_file("src/file1.txt")?;
    println!("processed file 1");

    let file2 = file::read_file("src/file2.txt")?;
    println!("processed file 2");

    dbg!(&file1);
    dbg!(&file2);

    Ok(())
}

使用cargo run編譯並運行程序。該程序應該毫無意外地運行,但是請確保已在src文件夾中放置了兩個文件(file1.txt和file2.txt)。

program started
processed file 1
processed file 2
[src/main.rs:14] &file1 = "file1"
[src/main.rs:15] &file2 = "file2"

到目前爲止,一切都很好。如果需要在處理file2.txt之前先處理file1.txt,那麼這是唯一的方法。但是有時不必關心每個文件的處理順序。理想情況下,希望儘快處理文件。

在這種情況下,我們可以利用多線程。
多線程方法

爲此,我們爲每個函數調用運行一個單獨的線程。由於我們使用的是多線程代碼,並且如果要訪問線程外部的文件內容,則必須使用Rust提供的同步原語之一。

這將如何影響代碼:file.rs將保持不變,因此這已經是一件好事了。在main.rs中,我們需要初始化兩個RwLock;這些將稍後在線程中用於存儲文件內容。

然後,我們運行一個無限循環,嘗試讀取這兩個變量的內容。如果這些變量不爲空,則我們知道文件處理(或讀取)已完成。 (這意味着文件不應爲空;否則,我們的程序將錯誤地保持等待狀態。另一種方法是使用Option並檢查Option是否爲None)。

此代碼需要crate lazy_static。

// multi-example/src/main.rs

use std::io;
use std::sync::RwLock;
use std::thread;

use lazy_static::lazy_static;

mod file;

// A sync primitive that allows to read/write from variables between threads.
// we declare the variables here, this requires the lazy_static crate
lazy_static! {
    static ref FILE1: RwLock<String> = RwLock::new(String::from(""));
    static ref FILE2: RwLock<String> = RwLock::new(String::from(""));
}

fn main() -> io::Result<()> {
    println!("program started");

    let thread_1 = thread::spawn(|| {
        let mut w1 = FILE1.write().unwrap();
        *w1 = file::read_file("src/file1.txt").unwrap();
        println!("read file 1");
    });

    println!("Launched Thread 1");

    let thread_2 = thread::spawn(|| {
        let mut w2 = FILE2.write().unwrap();
        *w2 = file::read_file("src/file2.txt").unwrap();
        println!("read file 2");
    });

    println!("Launched Thread 2");

    let mut rf1: bool = false;
    let mut rf2: bool = false;

    loop {
    	// read()
        let r1 = FILE1.read().unwrap();
        let r2 = FILE2.read().unwrap();

        if *r1 != String::from("") && rf1 == false {
            println!("completed file 1");
            rf1 = true;
        }

        if *r2 != String::from("") && rf2 == false {
            println!("completed file 2");
            rf2 = true;
        }
    }

    Ok(())
}

有趣的是,如果我們有一個非常大的file1.txt,我們將得到一個奇怪的輸出。首先處理第二個文件(讀取文件2);但在我們的循環內部,該程序似乎阻塞並等待第一個文件。

program started
Launched Thread 1
Launched Thread 2
read file 2
read file 1
completed file 1
completed file 2

多線程可能有點棘手,因爲我們必須考慮可能阻塞的原子操作。我們使用read函數來解鎖我們的變量,並且文檔對這種行爲發出警告。

使用共享的讀取訪問權限鎖定此rwlock,阻塞當前線程,直到可以獲取它爲止。

幸運的是,有一個try_read函數,如果無法獲取鎖,則返回Err。

嘗試使用共享的讀取訪問權限獲取此rwlock。

如果此時不能授予訪問權限,則返回Err。 否則,將返回RAII保護,當該保護被刪除時,該保護將釋放共享訪問。

在第二次嘗試中,我們使用try_read並忽略返回的Errs,因爲它們應該表示我們的鎖正忙。這有助於將程序移至下一個變量,並處理先準備好的變量。

// multi-example/src/main.rs

...
    loop {
    	// try_read()
        let r1 = FILE1.try_read();
        let r2 = FILE2.try_read();

        match r1 {
            Ok(v) => {
                if *v != String::from("") && rf1 == false {
                    println!("completed file 1");
                    rf1 = true;
                }
            }
            // If rwlock can't be acquired, ignore the error
            Err(_) => {}
        }

        match r2 {
            Ok(v) => {
                if *v != String::from("") && rf2 == false {
                    println!("completed file 2");
                    rf2 = true;
                }
            }
            // If rwlock can't be acquired, ignore the error
            Err(_) => {}
        }
    }

現在執行方式有所不同。如果file1.txt比file2.txt大得多,則應首先處理第二個文件。

program started
Launched Thread 1
Launched Thread 2
read file 2
completed file 2
read file 1
completed file 1

多線程的侷限性

如果我們已經有多線程,爲什麼我們需要異步?有兩個主要優點:性能和簡單性。產生線程很昂貴;從以上內容可以得出結論,編寫多線程代碼可能會變得非常複雜。
異步,關鍵字

Rust的重點是使編寫Async代碼儘可能簡單。只需要在函數聲明之前添加async/await關鍵字即可使代碼異步:函數聲明前async,解析異步函數await。

這聽起來很不錯。試一試吧。

use std::fs::File;
use std::io::{self, Read};

pub async fn read_file(path: &str) -> io::Result<String> {
    let mut file = File::open(path)?;
    let mut buffer = String::new();
    file.read_to_string(&mut buffer)?;
    Ok(buffer)
}

use std::io;

mod file;

fn main() -> io::Result<()> {
    let r1 = file::read_file("src/file1.txt");
    let r2 = file::read_file("src/file2.txt");

    let f1 = r1.await;
    let f2 = r2.await;

    dbg!(f1);
    dbg!(f2);

    Ok(())
}

但是這不能通過編譯,await僅在異步塊或函數中可用。如果我們嘗試運行此代碼,則編譯器將引發此錯誤。

error[E0728]: `await` is only allowed inside `async` functions and blocks
 --> src/main.rs:9:14
  |
5 | fn main() -> io::Result<()> {
  |    ---- this is not `async`
...
9 |     let f1 = r1.await;
  |              ^^^^^^^^ only allowed inside `async` functions and blocks

我們可以使main函數異步嗎?不幸的是,事情並非如此簡單。我們得到另一個錯誤。

error[E0277]: `main` has invalid return type `impl std::future::Future`
 --> src/main.rs:5:20
  |
5 | async fn main() -> io::Result<()> {
  |                    ^^^^^^^^^^^^^^ `main` can only return types that implement `std::process::Termination`
  |
  = help: consider using `()`, or a `Result`

但是,錯誤消息有點令人着迷。似乎async關鍵字使我們的函數返回Future而不是聲明的類型。

異步函數的返回類型是Future(確切地說是實現Future特性的閉包)。

那await呢?await在整個Future中循環直至完成。但是,還有另外一個謎團:Rust無法自解析Future。我們需要一個執行器來運行此異步代碼。
什麼是執行器?

如果回顧一下我們的多線程示例,會注意到我們使用循環來檢測何時處理文件。這很簡單:無限循環直到變量中包含某些內容,然後執行某些操作。如果讀取兩個文件,我們可以通過跳出循環來改善這一點。

一個異步執行器是循環。默認情況下,Rust沒有任何內置的執行程序。有許多異步運行時;async-std和Tokio是最受歡迎的。運行時的工作是輪詢異步函數(Future),直到它們最終返回一個值。
一個簡單的執行器

crate futures有一個非常基本的執行器,並且具有將兩個Future連接的函數。讓我們試一試。

以下代碼使用crate futures版本0.3.4。

// async-example/src/main.rs

use futures::executor::block_on;
use futures::join;
use std::io;

mod file;

fn main() -> io::Result<()> {

    println!("Program started");

    // Block on the final future
    block_on(load_files());

    Ok(())
}

async fn load_files() {
    // Join the two futures together
    join!(load_file_1(), load_file_2());
}

async fn load_file_1() {
    let r1 = file::read_file("src/file1.txt").await;
    println!("file 1 size: {}", r1.unwrap().len());
}

async fn load_file_2() {
    let r2 = file::read_file("src/file2.txt").await;
    println!("file 2 size: {}", r2.unwrap().len());
}

爲了驗證異步性,將一堆數據轉儲到file1.txt中。

Program started
file 1 size: 5399
file 2 size: 5

不幸的是,這看起來(確實)第一個文件函數再次阻塞了。
那麼異步到底是什麼?

與多線程類似,異步編程中也有一些陷阱和問題。事實是,async關鍵字不會神奇地使代碼異步;它只是使函數返回Future。仍然必須繁重地安排代碼執行時間。

這意味着函數必須迅速返回尚未準備就緒的狀態,而不是被困在進行計算的過程中。在我們的情況下,阻塞是特定在File::Open和file.read_to_string處發生的。這兩個函數不是異步的,因此會阻止執行。

我們需要創建這兩個函數的異步版本。幸運的是,一些使用async-std的人做了工作,將Rust中的std庫重寫爲異步版本。
使用async-std的文件IO

我們唯一要做的更改是將我們的std導入替換爲async_std。

對於以下示例,我們使用crate async-std版本1.5.0。

// async-example/src/file.rs

// We use async_std instead of std, it's that simple.
use async_std::io;
use async_std::fs::File;
use async_std::prelude::*;

pub async fn read_file(path: &str) -> io::Result<String> {
    let mut file = File::open(path).await?;
    let mut buffer = String::new();
    file.read_to_string(&mut buffer).await?;
    Ok(buffer)
}

main.rs中的代碼保持不變;該程序仍使用crate futures中的block_on執行程序。

編譯並運行程序。(確保有一個大的file1.txt)

Program started
file 2 size: 5
file 1 size: 5399

最後!程序首先快速處理file2.txt,然後移至file1.txt。

讓我們回顧一下到目前爲止所學到的東西:

async使我們的函數返回Future。

運行我們的Future需要一個運行時。

運行時檢查Future是否準備就緒;並在就緒時返回其值。

總結

在這篇文章中,我們介紹了同步代碼,多線程代碼,Rust中的一些異步術語,async-std庫和簡單的Future實現。實際上,這是一個"輕量級"的介紹,爲簡潔起見,省略了許多細節。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章