Rust对异步编程的支持

线程池与异步执行

作为一名嵌入式底层开发人员,工作中很少遇到线程池和异步执行的概念。在Lua脚本语言中有一个协程的概念,与线程池的异步执行有一些相似,但仍存在很多区别。用户态应用创建一个线程,绝大部分时间处于阻塞的状态(如果不是这样,这个线程占用的CPU时间会很高);线程会占用一定的系统资源。当一个多线程的应用的大部分线程都处于一个阻塞的状态,那就会浪费很多的系统资源。此外,对于一些高并发的事件处理,若采用创建新的任务线程的方案,那么某些情况下创建的线程数量可能达到上百个甚至上千个。这两点都会浪费操作系统的资源。我想这是线程池和异步执行能够解决的问题。通过内核的调度,一个进程或线程可以运行在任意一个CPU核上;而交给线程池的一段任务代码,也可能运行在池中的任意一个线程上。笔者编写了简单的Rust示例,可以验证这一点。

Rust的异步编程库:async_std

虽然Rust编程语言标准库中增加了异步执行的支持(通过future模块和task模块),但常用的异步编程接口是由async-std外部crate提供的。该库也依赖其他的库,但只需在Cargo.toml工程配置文件中例出async-std即可。该库提供了与标准库相似的模块,如fs/io/net等,这些模块提供的接口与标准库提供的接口也相似,主要的区别是前者是异步的,标准库提供的功能基本上都是同步的。

异步函数或异步代码块以async关键字标注,其返回会值(如果有)是一个异步值,标准库里称为future。等待异步值返回的操作是通过await关键字标注,它会等待一个future类型的值返回最终的结果。具体的实现是,await关键字会被编译成“轮询”操作,不过这个轮询操作不会阻塞,也不会忙等待;具体的机制类似于内核的任务调度和应用的协程。

简单异步功能的演示

为了探究Rust编程语言对异步执行的支持,笔者编写了简单测试代码。其中Cargo.toml的依赖项为:

libc = "0.2.99"
chrono = "0.4"
async-std = { version = "1.9", features = ["attributes"] }

主体代码通过async_std::task::spawn创建了4个异步任务;异步任务会输出自己的线程ID(从操作系统角度来看),并睡眠指定的时间:

use libc;
use chrono::Local;
use async_std::task;

fn get_thread_id() -> usize {
    let threadid = unsafe {
        libc::pthread_self() as usize
    };
    threadid
}

fn now_string() -> String {
    let now = Local::now();
    now.to_rfc2822()
}

async fn task_func(seconds: u64) -> usize {
    let mut thid = get_thread_id();
    println!("{} -> task running: {}, thread ID: {}",
        now_string(), seconds, thid);
    task::sleep(std::time::Duration::new(seconds, 0)).await;
    thid = get_thread_id();
    println!("{} -> task stopped: {}, thread ID: {}",
        now_string(), seconds, thid);
    thid
}

#[async_std::main]
async fn main() -> std::io::Result<()> {
    let mut handles = Vec::new();
    println!("Rust main function thread ID: {}",
        get_thread_id());
    for secs in 1..5 {
        handles.push(task::spawn(task_func(secs as u64)));
    }
    for handle in handles {
        let _ = handle.await;
    }
    println!("Rust main function thread ID: {}",
        get_thread_id());
    Ok(())
}

通过执行cargo build可以编译以上演示代码。编译时需要下载并编译超过20个依赖库,推荐使用国内的crates-io镜像源。编译完成后运行的结果如下:

Rust main function thread ID: 140262751298944
Sun, 15 Aug 2021 19:09:21 +0800 -> task running: 1, thread ID: 140262749189888
Sun, 15 Aug 2021 19:09:21 +0800 -> task running: 2, thread ID: 140262751291136
Sun, 15 Aug 2021 19:09:21 +0800 -> task running: 4, thread ID: 140262749189888
Sun, 15 Aug 2021 19:09:21 +0800 -> task running: 3, thread ID: 140262751291136
Sun, 15 Aug 2021 19:09:22 +0800 -> task stopped: 1, thread ID: 140262751291136
Sun, 15 Aug 2021 19:09:23 +0800 -> task stopped: 2, thread ID: 140262751291136
Sun, 15 Aug 2021 19:09:24 +0800 -> task stopped: 3, thread ID: 140262751291136
Sun, 15 Aug 2021 19:09:25 +0800 -> task stopped: 4, thread ID: 140262751291136
Rust main function thread ID: 140262751298944

由运行结果可知,至少两个不同的线程都执行了:task 1task 4。也就是说,对于异步执行的任务而言,通常的线程ID概念就不可用了:线程池的调度算法会根据实时的负载情况动态地调整,一个异步任务会被多个线程执行。

单线程的异步执行

使能async-std库的unstable特性,可以调用async_std::task::spawn_local函数,创建本地异步任务;这此任务是由调用该函数的线程执行的。主要改动有两处:

--- a/Cargo.toml
+++ b/Cargo.toml 
@@ -8,4 +8,4 @@
 [dependencies]
 libc = "0.2.99"
 chrono = "0.4"
-async-std = { version = "1.9", features = ["attributes"] }
+async-std = { version = "1.9", features = ["attributes", "unstable" ] }

--- a/src/main.rs
+++ b/src/main.rs
@@ -35,7 +35,7 @@
     println!("Rust main function thread ID: {}",
         get_thread_id());
     for secs in 1..5 {
-        handles.push(task::spawn(task_func(secs as u64)));
+        handles.push(task::spawn_local(task_func(secs as u64)));
     }

重新编译并运行,结果如下:

Rust main function thread ID: 139691300664704
Sun, 15 Aug 2021 20:43:35 +0800 -> task running: 1, thread ID: 139691300664704
Sun, 15 Aug 2021 20:43:35 +0800 -> task running: 2, thread ID: 139691300664704
Sun, 15 Aug 2021 20:43:35 +0800 -> task running: 3, thread ID: 139691300664704
Sun, 15 Aug 2021 20:43:35 +0800 -> task running: 4, thread ID: 139691300664704
Sun, 15 Aug 2021 20:43:36 +0800 -> task stopped: 1, thread ID: 139691300664704
Sun, 15 Aug 2021 20:43:37 +0800 -> task stopped: 2, thread ID: 139691300664704
Sun, 15 Aug 2021 20:43:38 +0800 -> task stopped: 3, thread ID: 139691300664704
Sun, 15 Aug 2021 20:43:39 +0800 -> task stopped: 4, thread ID: 139691300664704
Rust main function thread ID: 139691300664704

可见,各个异步任务获得的线程ID是相同的。这一功能与Lua脚本的协程功能类似,多个任务交替执行,却只用了单个线程。至此就对Rust编程语言的异步执行功能有了一个基本的认识了。

你可能感兴趣的:(杂谈,rust)