文章目录
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}");
}
}