一文讀懂Rust的async

不一樣的編程語言表現異步編程的方式可能不同,Rust跟JavaScript的async/await相似:使用的關鍵字啊,編程模型啊都差很少啦! 也有些不同的地方,畢竟Rust是從新設計的語言嘛,好比:在JavaScript中使用Promise表示須要延遲異步執行的計算,在Rust中使用的是Future.在JavaScript中不須要選擇指定運行異步代碼的運行時,在Rust中須要. Rust還更麻煩了?還得選擇指定運行時?web

這是由於Rust是能夠面向硬件、嵌入式設備的操做系統級別的編程語言就像C++,而且零抽象成本.這就須要Rust須要有選擇地把功能包含進標準庫裏.簡單來講,爲了知足不一樣的編程場景Rust標準庫就沒有包含指定異步代碼運行時,咱們能夠根據具體的場景選擇不一樣的運行時。編程

是否是感受還有點暈乎?不要緊,下面咱們會介紹怎麼在Rust中編寫異步(async)代碼.知道了怎麼編寫異步代碼,也就知道async是什麼了.若是你是第一次使用Rust編寫異步代碼或者第一次使用異步代碼庫正在迷茫從何入手,那恭喜你,這篇文檔特別適合你.開始以前咱們先快速的介紹下異步編程的基本要素.多線程

基本要素

編寫異步的應用,至少須要倆個crate:併發

  1. futures:這個是Rust官方團隊提供維護的crate.
  2. 異步代碼運行時crate: 能夠本身選擇,好比:Tokioasync_stdsmol等等.

你可能不想在項目中引入過多依賴,但這些依賴就像chronolog是比較基礎的依賴.惟一的不一樣是這些依賴是面向異步編程的.app

咱們接下來會使用Tokio作爲運行時,剛開始你最好也先了解熟悉使用一種運行時,而後再嘗試使用其它運行時。框架

由於這些運行時之間有不少相通的地方,熟悉了一個再去熟悉其它的就簡單了。就像咱們學習編程語言同樣,學好學深一門編程語言,再去學習其它的語言就快了。不要一開始就幾門語言一塊兒學,這樣極可能實際開發時這也不行那也不行換來換去仍是不能開發出東西.異步

咱們能夠像下面這樣引入依賴:async

[dependencies]
futures = { version = "0.3.*" }
tokio = {version = "0.2.*", features = ["full"] }

main.rs中敲入如下代碼:編程語言

use futures::prelude::*;
use tokio::prelude::*;

fn main() {
    todo!();
}

能夠執行下cargo check若是沒什麼報錯信息,依賴配置就完成了.接下來咱們介紹怎麼使用運行時。ide

運行時

像咱們先前說的Rust標準庫並無指定異步代碼的運行時,因此咱們本身選擇運行時並配置相應的依賴。這裏咱們選擇了使用Tokio:

tokio = {version = 「0.2.*」, features = [「full」] }

有些第三方庫可能須要咱們使用指定的異步代碼運行時,由於它們內部是對特定運行時庫進行了封裝。好比:web開發框架actix_web就是基於tokio封裝開發的.但大多少狀況咱們均可以本身選擇運行時。不管咱們選擇那一種運行時,在開始編寫代碼前都須要先搞清除:

  1. 怎麼啓動運行時?
  2. 怎麼生成 Future ?
  3. 怎麼處理阻塞(IO密集)和CPU密集任務?

搞清除了這三個問題基本上也就學會怎麼編寫異步代碼了.接下來咱們就以tokio爲例演示下:

  1. 啓動運行時

    能夠實例化一個運行時,並派生一個Future指定給運行時。這個Future就是異步代碼的主入口,能夠把它想象成異步代碼的main函數:

    async fn app() {
        todo!()
    }
    
    fn main() {
        let mut rt = tokio::runtime::Runtime::new().unwrap();
        let future = app();
        rt.block_on(future);
    }

    還可使用宏,簡化代碼爲:

    #[tokio::main]
    async fn main() {
    
    }

    雖然代碼行數少了,功能跟上面的代碼仍是同樣的哦!


  2. 爲運行時生成Future

    你想併發運行多個任務時,就能夠像這樣生成Future:

    use tokio::task;
    
    async fn our_async_program() {
        todo!();
    }
    
    async fn app() {
        let concurrent_future = task::spawn(our_async_program());
        todo!()
    }

  3. 處理阻塞和CPU密集性任務

    什麼是阻塞性的任務?什麼是CPU密集性的任務呢?能夠簡單的理解爲這兩種任務都會長時間的霸佔CPU阻塞線程繼續執行其它任務.就比如工地上有個包工頭專門負責分配任務給小工門幹,有些小活小任務包工頭可能順手就幹了,可是一些耗時比較長的好比去搬一車磚頭,包工頭就不能本身去幹了,由於它去搬磚頭了就沒人負責任務分配了,小工們活都幹完了只能等着包工頭分配任務才能繼續幹活.包工頭呢?還在搬磚頭呢.顯然這是會影響總體工做效率的。代碼也同樣,要有個專門負責整體分配任務的線程,在這個線程中就不能再執行其它比較耗費時間的的任務了。那耗費時間的任務誰來執行呢?小工唄,也就是派生新的Future. 就像這個樣子:

    use tokio::task;
    
    fn fib_cpu_intensive(n: u32) -> u32 {
        match n {
            0 => 0,
            1 => 1,
            n => fib_cpu_intensive(n - 1) + fib_cpu_intensive(n - 2),
        }
    }
    
    async fn app() {
        let threadpool_future = task::spawn_blocking(||fib_cpu_intensive(30));
        todo!()
    }

    tokio是使用的spawn_blocking去派生新的Future使用新的線程執行比較耗時的任務,其它運行時庫可能API不同但也會提供相似的方法.


異步開發樣例

支持咱們已經學習了怎麼使用Rust編寫異步代碼,接下來把所學內容整合到一塊兒作個樣例:

use futures::prelude::*;
use tokio::prelude::*;
use tokio::task;
use log::*;

// Just a generic Result type to ease error handling for us. Errors in multithreaded
// async contexts needs some extra restrictions
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

async fn app() -> Result<()> {
    // I treat this as the `main` function of the async part of our program. 
    todo!()
}

fn main() {
    env_logger::init();
    let mut rt = tokio::runtime::Runtime::new().unwrap();

    match rt.block_on(app()) {
        Ok(_) => info!("Done"),
        Err(e) => error!("An error ocurred: {}", e),
    };
}

使用到的crate有:

  • 提供異步代碼運行時的 tokio
  • Rust日誌門面log
  • 日誌工具env_logger

Cargo.toml相似這個樣子:

[dependencies]
futures = { version = "0.3.*"}
tokio = {version = "0.2.*", features = ["full"] }
log = "0.4.*"
env_logger = "0.7.*"

須要注意的是env_logger須要根據環境變量RUST_LOG設置日誌級別

基本上全部的異步編程項目均可以使用相似這樣的依賴配置和main.rs.根據不一樣的使用場景還能夠優化下錯誤處理和日誌.好比能夠考慮使用 Anyhow處理錯誤,能夠考慮使用 async-log更好的在異步多線程環境中輸出日誌.在本文檔中接下來的代碼就基於這個樣例模板開發了。

異步函數

在Rust中編寫異步函數跟先前編寫普通函數有點不同.先前接觸Rust函數時,你可能已經注意到函數的參數返回值都須要聲明確切的類型。異步函數的返回值都是通過Future包裝的。若是你讀了關於Future的文檔,按照這個思路你可能認爲應該像下面這樣編寫異步函數:

async fn our_async_program() -> impl Future<Output = Result<String>> {
    future::ok("Hello world".to_string()).await
}

不用這麼麻煩,比較Rust是從新設計的語言.當你使用async關鍵字時,Rust會自動的使用Future封裝返回只,因此你原來怎麼給普通函數定義返回值就繼續那麼地幹,就像這個樣子:

async fn our_async_program() -> Result<String> {
    future::ok("Hello world".to_string()).await
}

這裏使用的future::ok是future庫提供的方便咱們開發使用的,用於生成狀態爲readyfuture.

你可能會見到使用異步代碼塊async {...}建立異步代碼的,這是爲了更靈活的定義返回值類型,不過大多少狀況下使用異步函數就夠了.接下來咱們編寫一個使用異步函數的例子.

建立Web請求

在Rust中futurelazy(懶)的.也就是說,默認狀況下當你建立了一個future,它是什麼都不幹的,非得等你調用await告訴它該幹活了,它纔開始幹活.

接下來咱們以發起處理Web請求的場景用代碼演示一會兒:

fn slowwly(delay_ms: u32) -> reqwest::Url {
    let url = format!(
    "http://slowwly.robertomurray.co.uk/delay/{}/url/http://www.google.co.uk", 
    delay_ms,
    );
    reqwest::Url::parse(&url).unwrap()
}

async fn app() -> Result<()> {
    info!("Starting program!");
    let _resp1 = reqwest::get(slowwly(1000)).await?;
    info!("Got response 1");
    let _resp2 = reqwest::get(slowwly(1000)).await?;
    info!("Got response 2");
    Ok(())
}

建立web請求使用到了reqwest庫,須要把這個庫添加到Cargo.toml的依賴區域:

reqwest = 「0.10.*」

執行上面的代碼輸出相似這個樣子:

1.264 [INFO] - Got response 1
2.467 [INFO] - Got response 2
2.468 [INFO] - Done

這裏的日誌格式是自定義的,前面的數字是程序執行的時間,自定義日誌格式的代碼是這個樣子地:

et start = std::time::Instant::now();
env_logger::Builder::from_default_env().format(move |buf, rec| {
    let t = start.elapsed().as_secs_f32();
    writeln!(buf, "{:.03} [{}] - {}", t, rec.level(),rec.args())
}).init();

從日誌輸出能夠看出,咱們的函數並非一塊兒執行的,而是一個執行完成後另外一個纔開始執行的,由於咱們這裏仍是使用的普通函數並無使用異步函數.接下來是修改成異步函數的版本:

async fn request(n: usize) -> Result<()> {
    reqwest::get(slowwly(1000)).await?;
    info!("Got response {}", n);
    Ok(())
}

async fn app() -> Result<()> {
    let resp1 = task::spawn(request(1));
    let resp2 = task::spawn(request(2));

    let _ = resp1.await??;
    let _ = resp2.await??;

    Ok(())
}

tokio提供的spawn函數可讓咱們使用多線程併發執行異步函數.

執行的效果相似這個樣子:

1.247 [INFO] - Got response 2
1.256 [INFO] - Got response 1
1.257 [INFO] - Done

能夠跟上面使用普通函數的方式對比一會兒,是否是整體效率快多了,倆個請求不須要互相等待,各自說幹就幹,就是這麼快.

補充

何時該派生Future執行任務呢?這裏有幾條建議

  1. 優先選用沒有阻塞的操做庫.
  2. 若是不肯定就派生一個吧.
相關文章
相關標籤/搜索