详解Rust异步编程

文章目录

Rust的异步编程通过async/await语法和Future特性提供了一种高效的方式来处理并发任务,尤其在I/O密集型操作中表现出色。async/await异步编程模型性能高,还能支持底层编程,同时又像线程和协程那样无需过多的改变编程模型,但async模型的问题就是内部实现机制过于复杂,对于用户来说,理解和使用没有线程和协程简单。

多线程编程与异步编程对比

Rust的多线程编程和异步编程都是处理并发的常用方式,虽然它们都能够提高程序的并发性能,但它们在实现原理、使用场景、优缺点等方面存在一些重要差异。

1.概念区别
多线程编程

Rust的多线程编程利用操作系统的线程来并行执行任务,每个线程都有自己的执行上下文和栈。Rust通过std::thread模块来创建和管理线程。线程间的共享数据需要通过锁或原子操作来管理,以避免数据竞态。

异步编程

Rust的异步编程基于非阻塞I/O操作,并通过async/await语法实现。异步任务通常是在单线程中通过事件循环和任务调度来实现并发,而不是通过多个操作系统线程。Rust的异步编程主要依赖于Future和Tokio、async-std等库来管理和调度任务。

2.实现机制

多线程编程,每个线程都由操作系统调度,独立执行任务。线程通常会阻塞,直到执行完成,线程间的数据共享需要显式地通过Arc<Mutex>、RwLock或 Atomic等方式来进行同步。

异步函数是基于事件循环和任务调度器的,执行时不会阻塞线程,而是通过协作式多任务调度实现并发。当遇到需要等待的操作(如 I/O、网络请求等)时,异步任务会主动让出控制权,直到操作完成才会继续执行。异步编程依赖于Future和.await来控制任务的调度和执行。

3.使用场景

多线程编程适用于计算密集型任务,如大规模数据处理、图像处理、视频渲染等。当任务需要大量CPU资源并且任务之间的执行是独立的时,使用多线程能够显著提升性能。适合任务需要真实并行执行的场景,比如将任务分配到多个CPU核心上运行。

异步编程适用于I/O密集型任务,如网络请求、文件操作、数据库访问等。当任务的瓶颈在于等待外部资源时,异步编程能够显著提升效率。用于高并发的Web服务器、网络客户端等应用,特别是当大量连接/请求需要同时处理时,异步编程的优势非常明显。

有大量IO任务需要并发运行时,选async模型

有部分IO任务需要并发运行时,选多线程,如果想要降低线程创建和销毁的开销,可以使用线程池

有大量CPU密集任务需要并行运行时,例如并行计算,选多线程模型,且让线程数等于或者稍大于CPU核心数

无所谓时统一选多线程

4.优缺点对比

种类 优点 缺点
多线程编程 1.真正的并行执行,适合CPU密集型任务 2.线程间独立,控制直观,易于理解; 3.无数据竞争和内存问题是安全的 1.创建和销毁线程的成本较高,过多线程可能导致上下文切换的开销。 2.线程间同步需要额外处理,增加了复杂性。 3.线程调度由操作系统管理,不能完全控制线程执行顺序。
异步编程 1.适合I/O密集型任务,能在单线程上处理大量并发任务,避免了线程创建和上下文切换的开销。 2.异步编程通过事件循环调度,不需要操作系统线程支持,因此能在较低的系统资源下运行。 3.通过async/await语法,代码更简洁、易于理解 1.无法有效利用CPU,不适合CPU密集型任务。 2.异步代码可能会引入潜在的生命周期和借用问题。 3.异步编程对调度器和运行时(如 Tokio 或 async-std)有一定依赖,这可能增加外部库的复杂性

5.性能对比

多线程每个线程都是由操作系统调度的,具有独立的栈和上下文,因此能够实现真正的并行。在多核CPU上适合处理计算密集型任务。然而线程创建和销毁的开销相对较大。

异步操作在单线程中,通过任务调度来处理并发,可以避免线程的创建和销毁开销。适合I/O密集型任务,但对CPU密集型任务的性能提升有限,可能需要结合多线程或多进程来解决。

async和多线程的性能对比

操作 async 线程
创建 0.3 微秒 17 微秒
线程切换 0.2 微秒 1.7 微秒

并发模型对比分析

对比分析各种并发模型的优缺点及适用场景。

并发模型 优点 缺点 适用场景
OS 线程 简单直接,原生支持,易于理解,不需要改变编程模型 上下文切换损耗大,线程间同步困难,性能对 I/O 密集型场景不理想 适合 CPU 密集型任务、并行计算
事件驱动 性能高,处理并发时非常高效 回调地狱,非线性控制流导致数据流向和错误传播难以控制,降低可维护性 适合 I/O 密集型任务,尤其是网络服务等
协程 支持大量并发任务,性能高,易于实现并发编程 抽象层次过高,无法触及底层细节,系统编程和自定义异步运行时难用 适合需要大量并发任务的场景,但不涉及底层系统编程
Actor 模型 贴近现实,易于实现并发计算,消息传递模式适合分布式系统设计 流控制、失败重试等复杂场景下不太好用 适合分布式系统、松耦合的并发计算场景
async/await 高效性能,支持底层编程,同时具备线程和协程的特点,无需改变编程模型 实现复杂,理解和使用有一定难度,但已有封装 适合高并发、异步 I/O 的场景,尤其是需要精细控制并发行为时

async是Rust选择的异步编程模型

异步编程基础概念及用法

1.async函数与await

通过将函数标记为async。Rust会将其转换为返回Future类型的函数。Future是Rust中表示异步操作的核心类型,表示一个尚未完成但可能会在将来完成的计算。

await用于挂起当前任务,直到一个Future完成并返回结果。调用await时当前任务会被暂停,直到Future完成并返回结果。await并不会阻塞当前的线程,而是异步的等待Future的完成。

有两种方式可以使用async: async fn用于声明函数,async { ... }用于声明语句块,它们会返回一个实现Future特征的值.

rust 复制代码
//该函数返回一个Future<i32>  
async fn foo() -> i32 {
    42
}
fn bar() -> impl Future<Output = u8> {
    // 下面的async语句块返回Future<Output = u8>  
    async {
        let x: u8 = foo().await;
        x + 5
    }
}

async fn bar() {
    //block_on会阻塞当前线程  
    //let future = foo();
    //block_on(future); 

    //与block_on不同.await并不会阻塞当前的线程
    let result = foo().await;
    println!("Result: {}", result);
}

2.Future类型

在Rust中异步操作通过Future类型表示,Future本身定义了一个状态机,它跟踪操作的进度。一个Future可以处于以下状态:

  • Pending: 操作正在进行中,尚未完成。
  • Ready: 操作已完成,具有结果值。

Future是惰性执行的,意味着它不会在创建时立即运行,而是在调用await或通过poll方法驱动执行时才开始执行

3.async/await的工作机制

async函数在编译时被转换为一个状态机。编译器会为每个await操作生成一个状态转换的过程,这样可以有效地管理执行流程而不阻塞线程,await操作会挂起函数,直到被等待的Future完成。这个过程并不会阻塞当前线程,而是通过poll的方式让任务调度器在适当时机恢复任务。

4.异步执行模型与任务调度

Rust本身并不提供内置的异步运行时,它依赖于外部库(例如Tokio和async-std)来提供任务调度和执行。常见的异步执行模型如下:

单线程模型: 许多异步框架使用一个单线程执行所有异步任务的调度器。在这个模型中,调度器在后台执行多个任务,尽量避免阻塞增加效率。

多线程模型: 某些框架(如Tokio)支持多线程模型,其中多个线程可以同时运行异步任务。

Tokio调用方法

ini 复制代码
# Cargo.toml
# 配置依赖库  
[dependencies]
tokio = { version = "1", features = ["full"] }
rust 复制代码
//使用Tokio运行时
use tokio::time::{sleep, Duration};

async fn hello_world() {
    println!("Hello");
    sleep(Duration::from_secs(1)).await;
    println!("World");
}

#[tokio::main]
async fn main() {
    hello_world().await;
}

5.异步错误处理

Rust的异步错误处理与同步代码相似,使用Result和Option类型。异步函数通常会返回Result类型。

rust 复制代码
async fn might_fail() -> Result<(), String> {
    // Some async operation that might fail
    Err("Something went wrong".to_string())
}

#[tokio::main]
async fn main() {
    match might_fail().await {
        Ok(_) => println!("Success"),
        Err(e) => println!("Error: {}", e),
    }
}

6.并发与并行

Rust的异步模型能够在单线程中并发执行多个异步任务,这意味着即使你只有一个线程,异步任务依然可以并发执行,但它们实际上是通过时间片轮转来实现的。

如果需要并行(例如在多个CPU核心上运行任务),你可以使用多线程运行时(如Tokio或async-std)。

rust 复制代码
use tokio::task;

async fn task1() {
    println!("Task 1 started");
    // simulate async work
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
    println!("Task 1 done");
}

async fn task2() {
    println!("Task 2 started");
    // simulate async work
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
    println!("Task 2 done");
}

#[tokio::main]
async fn main() {
    let t1 = task::spawn(task1());
    let t2 = task::spawn(task2());
    let _ = tokio::try_join!(t1, t2); // Wait for both tasks to complete
}

7.异步流与通道

Rust还提供了异步流(Stream)和通道(Channel)来处理更复杂的异步场景,例如处理一系列异步数据流或者在不同任务之间传递消息。

异步流(Stream): 表示一系列异步值的集合,可以通过Stream提供的next()方法来异步地获取这些值。

通道(Channel): 异步通道允许不同任务之间传递数据。常用的通道库包括tokio::sync::mpsc和async-std::channel。

Stream类似于Future但是它可以生成多个值,直到它完成。它的行为与标准库中的Iterator很像。Stream trait定义了poll_next 方法,用于返回流中的下一个元素,返回值为Poll<Option>。

  • Poll::Pending: 表示流还没有数据。
  • Poll::Ready(Some(item)): 表示流有数据。
  • Poll::Ready(None): 表示流已完成,没有更多数据。
rust 复制代码
async fn send_recv() {
    const BUFFER_SIZE: usize = 10;
    let (mut tx, mut rx) = mpsc::channel::<i32>(BUFFER_SIZE);

    tx.send(1).await.unwrap();
    tx.send(2).await.unwrap();
    drop(tx);

    assert_eq!(Some(1), rx.next().await);
    assert_eq!(Some(2), rx.next().await);
    assert_eq!(None, rx.next().await);
}

8.性能分析

Rust的异步编程具有高效性,特别是在处理I/O密集型任务时。由于Rust的所有权系统和无垃圾回收机制,异步任务的内存管理得到了很好的保证,这使得Rust异步代码非常高效。

  • 无堆分配的异步任务: Rust提供了Pin和Box等类型来确保异步任务的内存位置不会发生变化,避免了运行时的额外开销。
  • 零成本抽象: Rust的异步编程模型通过编译时优化,提供了与同步代码几乎相同的性能,而不会引入额外的运行时开销。

9.同时运行多个Future

rust 复制代码
//两个future 一个先运行 另一个后运行  
async fn enjoy_book_and_music() -> (Book, Music) {
    let book = enjoy_book().await;
    let music = enjoy_music().await;
    (book, music)
}

//发运行两个 Future
//如果希望同时运行一个数组里的多个异步任务,可以使用 futures::future::join_all 方法  
use futures::join;
async fn enjoy_book_and_music() -> (Book, Music) {
    let book_fut = enjoy_book();
    let music_fut = enjoy_music();
    join!(book_fut, music_fut)
}

//希望在某一个Future报错后就立即停止所有Future的执行,可以使用 try_join!  
//有一点需要注意传给try_join!的所有Future都必须拥有相同的错误类型。
//如果错误类型不同,可以考虑使用来自 futures::future::TryFutureExt 模块的 map_err 和 err_info 方法将错误进行转换  
use futures::try_join;
async fn get_book() -> Result<Book, String> { /* ... */ Ok(Book) }
async fn get_music() -> Result<Music, String> { /* ... */ Ok(Music) }

async fn get_book_and_music() -> Result<(Book, Music), String> {
    let book_fut = get_book();
    let music_fut = get_music();
    try_join!(book_fut, music_fut)
}

10.async函数的生命周期问题

当异步函数接受引用类型的参数时Future的生命周期必须至少与参数的生命周期相同。否则编译器会报错。

解决方法:通过将引用传入async语句块内,使其生命周期延续到Future返回时,避免生命周期不匹配的问题。

async move会捕获外部变量并将其所有权转移到异步任务中。这解决了借用生命周期的问题,避免了变量在任务完成前被释放。

使用async move时,所有的捕获变量的所有权会被转移,且该变量不再受到生命周期的限制,无法与其他代码共享。

rust 复制代码
// 多个不同的async语句块可以访问同一个本地变量 只要它们在该变量的作用域内执行
async fn blocks() {
    let my_string = "foo".to_string();

    let future_one = async {
        // ...
        println!("{my_string}");
    };

    let future_two = async {
        // ...
        println!("{my_string}");
    };
    // 运行两个 Future 直到完成
    let ((), ()) = futures::join!(future_one, future_two);
}

//由于async move会捕获环境中的变量,因此只有一个async move语句块可以访问该变量
//有非常明显的好处: 变量可以转移到返回的 Future 中,不再受借用生命周期的限制
fn move_block() -> impl Future<Output = ()> {
    let my_string = "foo".to_string();
    async move {
        // ...
        println!("{my_string}");
    }
}
相关推荐
SomeB1oody3 小时前
【Rust自学】4.1. 所有权:栈内存 vs. 堆内存
开发语言·后端·rust
SomeB1oody18 小时前
【Rust自学】4.2. 所有权规则、内存与分配
开发语言·后端·rust
SomeB1oody18 小时前
【Rust自学】4.5. 切片(Slice)
开发语言·后端·rust
编码浪子1 天前
构建一个rust生产应用读书笔记6-拒绝无效订阅者02
开发语言·后端·rust
baiyu331 天前
1小时放弃Rust(1): Hello-World
rust
baiyu331 天前
1小时放弃Rust(2): 两数之和
rust
Source.Liu1 天前
数据特性库 前言
rust·cad·num-traits
编码浪子1 天前
构建一个rust生产应用读书笔记7-确认邮件1
数据库·rust·php
SomeB1oody1 天前
【Rust自学】3.6. 控制流:循环
开发语言·后端·rust
Andrew_Ryan1 天前
深入了解 Rust 核心开发团队:这些人如何塑造了世界上最安全的编程语言
rust