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