[rust]异步编程

4 阅读6分钟

介绍

use futures::executor::block_on;
async fn hello_world() {
    println!("hello, world!");
}

fn main() {
    let future = hello_world(); // 返回一个Future, 因此不会打印任何输出
    block_on(future); // 执行`Future`并等待其运行完成,此时"hello, world!"会被打印输出
}
  1. 使用async关键字定义一个异步函数,并返回一个实现了Future特征的值,Future理解为一个在未来某个时间点被调度执行的任务,所以直接调用async函数不是直接返回结果。
  2. 使用block_on这个执行器( executor )执行Future
  3. block_on会阻塞当前线程直到指定的Future执行完成,这种阻塞当前线程以等待任务完成的方式较为简单、粗暴。好在其它运行时的执行器(executor)会提供更加复杂的行为,例如将多个future调度到同一个线程上执行。

await

  1. await 用于在异步函数中等待异步操作完成。它会暂停当前的异步函数,并允许其他任务运行,直到等待的操作完成。
  2. await 只能在 async 函数或 async 块中使用。

错误示例

async fn hello_world() {
    hello_cat();
    println!("hello, world!");
}

async fn hello_cat() {
    println!("hello, kitty!");
}

fn main() {
    let future = hello_world();
    block_on(future); // 只输出了 "hello, world!"
}

正确示例

async fn hello_world() {
    hello_cat().await;
    println!("hello, world!");
}

async fn hello_cat() {
    println!("hello, kitty!");
}

fn main() {
    let future = hello_world();
    block_on(future);
}

// 输出:
// hello, kitty!
// hello, world!

证明 FeatureB 是在 FeatureA 运行后才被继续执行的

use tokio::time::{sleep, Duration};
async fn hello_world() { // FeatureB
    println!("task 1 begin: {:?}-{}", thread::current().id(), Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
    hello_cat().await;
    println!("task 1 end: {:?}-{}", thread::current().id(), Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
    println!("hello, world!");
}

async fn hello_cat() { // FeatureA
    sleep(Duration::from_secs(2)).await; // 模拟耗时
    println!("hello, kitty!");
    println!("task2 end: {:?}", thread::current().id())
}

#[tokio::main]
async fn main() {
    println!("main begin: {:?}", thread::current().id());
    let future = hello_world();
    block_on(future); // 只输出了 "hello, world!"
    println!("main end: {:?}", thread::current().id());
}

// 输出:
// main begin: ThreadId(1)
// task 1 begin: ThreadId(1)-2024-10-07 22:56:06
// hello, kitty!
// task2 end: ThreadId(1)
// task 1 end: ThreadId(1)-2024-10-07 22:56:08
// hello, world!
// main end: ThreadId(1)

在异步编程中,休眠操作不是阻塞当前线程,而是将控制权让出给其他任务,这样可以实现高效的并发处理,但是在FeatureA执行期间被休眠时,FeatureB并没有继续运行,因为两个任务都运行在同一个线程中

处理多个Future

并行处理

async fn task1() -> &'static str {
    println!("task 1 begin: {:?}-{}", thread::current().id(), Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
    sleep(Duration::from_secs(1)).await;
    println!("task 1 end: {:?}-{}", thread::current().id(), Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
    "Task 1 completed"
}

async fn task2() -> &'static str {
    println!("task 1 begin: {:?}-{}", thread::current().id(), Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
    sleep(Duration::from_secs(2)).await;
    println!("task 1 end: {:?}-{}", thread::current().id(), Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
    "Task 2 completed"
}

#[tokio::main]
async fn main() {
    let (res1, res2) = tokio::join!(task1(), task2());
    println!("{}, {}", res1, res2); // Task 1 completed, Task 2 completed
}

// 输出:
// task 1 begin: ThreadId(1)-2024-10-07 23:00:58
// task 1 begin: ThreadId(1)-2024-10-07 23:00:58
// task 1 end: ThreadId(1)-2024-10-07 23:00:59
// task 1 end: ThreadId(1)-2024-10-07 23:01:00
// Task 1 completed, Task 2 completed

顺序执行

#[tokio::main]
async fn main() {
    let res1 = task1().await;
    println!("{}", res1);

    let res2 = task2().await;
    println!("{}", res2);
}

// 输出:
// task 1 begin: ThreadId(1)-2024-10-07 23:02:11
// task 1 end: ThreadId(1)-2024-10-07 23:02:12
// Task 1 completed
// task 1 begin: ThreadId(1)-2024-10-07 23:02:12
// task 1 end: ThreadId(1)-2024-10-07 23:02:14
// Task 2 completed

竞态任务

#[tokio::main]
async fn main() {
    let result = tokio::select! {
        res = task1() => res,
        res = task2() => res,
    };

    println!("First completed task: {}", result);
}

// 输出:
// task 1 begin: ThreadId(1)-2024-10-07 23:03:45
// task 1 begin: ThreadId(1)-2024-10-07 23:03:45
// task 1 end: ThreadId(1)-2024-10-07 23:03:46
// First completed task: Task 1 completed
  1. 可能需要处理竞态任务,即同时启动多个 Future,并在第一个完成时返回结果
  2. tokio::select! 将等待两个任务中的第一个完成,并输出结果

并行处理Feature集合

use rand::Rng;
async fn task(n: u64) -> u64 {
    println!("task {} begin: {:?}-{}", n, thread::current().id(), Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
    sleep(Duration::from_secs(rand::thread_rng().gen_range(1..=5))).await;
    println!("task {} end: {:?}-{}", n, thread::current().id(), Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
    n + 1
}

#[tokio::main]
async fn main() {
    let futures: Vec<_> = (1..=5).map(task).collect();
    let results = join_all(futures).await;

    println!("Results: {:?}", results);
}

// 输出:
// task 1 begin: ThreadId(1)-2024-10-07 23:14:29
// // task 2 begin: ThreadId(1)-2024-10-07 23:14:29
// // task 3 begin: ThreadId(1)-2024-10-07 23:14:29
// // task 4 begin: ThreadId(1)-2024-10-07 23:14:29
// // task 5 begin: ThreadId(1)-2024-10-07 23:14:29
// // task 5 end: ThreadId(1)-2024-10-07 23:14:31
// // task 4 end: ThreadId(1)-2024-10-07 23:14:32
// // task 1 end: ThreadId(1)-2024-10-07 23:14:34
// // task 2 end: ThreadId(1)-2024-10-07 23:14:34
// // task 3 end: ThreadId(1)-2024-10-07 23:14:34
// // Results: [2, 3, 4, 5, 6]
  1. Future将输入值加1
  2. join_all 会并行执行 Future 集合中的所有任务,并返回一个包含每个 Future 结果的 Vec
  3. 执行过程并非是串行的,在当前执行的 Future 休眠的过程中,当前线程并不会阻塞,而是继续执行其他线程

串行Stream处理Feature集合

use tokio::time::{sleep, Duration};
use tokio_stream::{StreamExt, iter};
use rand::Rng;

#[tokio::main]
async fn main() {
    let tasks = (1..=5).map(task);
    let mut stream = iter(tasks);

    while let Some(result) = stream.next().await {
        println!("Task completed with result: {}", result.await.to_string());
    }
}

// 输出:
// task 1 begin: ThreadId(1)-2024-10-07 23:22:33
// task 1 end: ThreadId(1)-2024-10-07 23:22:35
// Task completed with result: 2
// task 2 begin: ThreadId(1)-2024-10-07 23:22:35
// task 2 end: ThreadId(1)-2024-10-07 23:22:37
// Task completed with result: 3
// task 3 begin: ThreadId(1)-2024-10-07 23:22:37
// task 3 end: ThreadId(1)-2024-10-07 23:22:38
// Task completed with result: 4
// task 4 begin: ThreadId(1)-2024-10-07 23:22:38
// task 4 end: ThreadId(1)-2024-10-07 23:22:40
// Task completed with result: 5
// task 5 begin: ThreadId(1)-2024-10-07 23:22:40
// task 5 end: ThreadId(1)-2024-10-07 23:22:41
// Task completed with result: 6
  1. 使用 tokio-stream 提供的 Stream 处理功能来持续处理多个 Future 生成的结果
  2. 异步流中的任务会一个接一个地执行,并处理每个任务的结果