Day 25: 异步编程入门
前面我们学习了多线程并发。多线程虽然强大,但有其局限性:创建线程有开销,线程间切换也有成本。对于IO密集型任务(如网络请求、文件读写),线程的效率不高。
异步编程(async/await)提供了一种更高效的并发模型:多个任务可以在少量线程上运行,当一个任务等待IO时,runtime切换到其他任务继续执行。
Rust的标准库提供了async/await语法和Future trait,但runtime需要借助第三方库来实现。今天我们学习async编程的基础知识。
2. async/await语法
Section titled “2. async/await语法”async将一个函数变成返回Future的异步函数,await用于等待Future完成:
// 这是同步函数fn sync_function() -> String { String::from("同步结果")}
// 这是异步函数,返回impl Futureasync fn async_function() -> String { String::from("异步结果")}
fn main() { // async函数返回的是一个Future,需要被执行器驱动 let future = async_function(); println!("创建了Future");}注意:async函数本身不会执行任何代码,它只是返回一个Future。只有当Future被poll(轮询)时,代码才会运行。
3. Future trait
Section titled “3. Future trait”Future是Rust异步编程的核心,表示一个可能还未完成的异步计算:
pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;}
pub enum Poll<T> { Ready(T), // Future已完成,结果可用 Pending, // Future还未完成,需要继续等待}Pin和Context是Future实现的细节:Pin用于固定自引用结构,Context提供Waker用于任务唤醒。
4. tokio:最流行的异步runtime
Section titled “4. tokio:最流行的异步runtime”Rust标准库只定义了Future trait,没有提供执行器(executor)。社区有多个async runtime实现,其中tokio是最流行的。
在Cargo.toml中添加依赖:
[dependencies]tokio = { version = "1", features = ["full"] }使用tokio运行async代码:
use tokio;
#[tokio::main]async fn main() { println!("tokio runtime启动");
// async函数直接调用 my_async_function().await;
println!("程序结束");}
async fn my_async_function() { println!("这是异步函数");}#[tokio::main]宏会生成一个main函数,内部创建tokio runtime并执行async main。
5. async/.await的实际例子
Section titled “5. async/.await的实际例子”让我们看一个更有意义的例子,使用tokio的网络功能:
use tokio::io::{AsyncReadExt, AsyncWriteExt};use tokio::net::TcpStream;use tokio::time::{sleep, Duration};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // 并发执行多个异步任务 let handle1 = tokio::spawn(async { sleep(Duration::from_millis(100)).await; "任务1完成" });
let handle2 = tokio::spawn(async { sleep(Duration::from_millis(50)).await; "任务2完成" });
// await两个任务 let result1 = handle1.await?; let result2 = handle2.await?;
println!("{} - {}", result1, result2);
Ok(())}tokio::spawn创建了一个新任务(task),可以在多个任务间高效切换。
6. tokio::spawn与线程的区别
Section titled “6. tokio::spawn与线程的区别”tokio::spawn创建的task与thread::spawn有本质区别:
use tokio::time::{sleep, Duration};use std::time::Instant;
#[tokio::main]async fn main() { let start = Instant::now();
// 创建1000个异步任务 let mut handles = vec![]; for i in 0..1000 { handles.push(tokio::spawn(async move { sleep(Duration::from_millis(10)).await; i })); }
// 等待所有任务完成 let mut total = 0; for handle in handles { total += handle.await.unwrap(); }
println!("总耗时: {:?}", start.elapsed()); println!("总和: {}", total);}如果是线程,需要1000个线程,内存开销巨大。而tokio只需要少量线程(默认CPU核数),就能高效运行这1000个任务。
7. 使用channel在async任务间通信
Section titled “7. 使用channel在async任务间通信”和线程类似,async任务也可以用channel通信:
use tokio::sync::mpsc;use tokio::time::{sleep, Duration};
#[tokio::main]async fn main() { let (tx, mut rx) = mpsc::channel(32); // 缓冲区大小32
// 发送者任务 let tx_clone = tx.clone(); tokio::spawn(async move { for i in 0..10 { tx_clone.send(i).await.unwrap(); sleep(Duration::from_millis(10)).await; } });
// 接收者任务 tokio::spawn(async move { while let Some(msg) = rx.recv().await { println!("收到: {}", msg); } });
// 等待一段时间让任务完成 sleep(Duration::from_secs(2)).await;}tokio的channel和std的channel接口类似,但操作都是async的。
8. async/.await的限制
Section titled “8. async/.await的限制”async函数有一些限制需要了解:
#[tokio::main]async fn main() { // async闭包可以使用.await let future = async { let result = some_async_function().await; result };
// 但不能在同步函数中调用async函数 // synchronous_function(); // 同步函数 // some_async_function().await; // 编译错误!
future.await;}
async fn some_async_function() -> u32 { 42}
fn synchronous_function() { println!("这是同步函数");}async函数不能从同步函数中调用。如果需要从同步代码调用async代码,可以使用block_on:
use tokio::runtime::Runtime;
fn main() { let rt = Runtime::new().unwrap(); rt.block_on(async { println!("从同步函数调用async代码"); });}今天我们入门了Rust的异步编程:
- async/.await 语法定义了异步函数和异步等待点
- Future trait 是异步计算的核心,需要被executor轮询执行
- tokio 是最流行的async runtime,提供了高效的task调度
- tokio::spawn 创建异步任务,比thread::spawn更轻量
- tokio::sync::channel 提供任务间通信
异步编程是现代高性能服务器和网络服务的基石。Rust的async设计与语言的其他特性(如所有权、生命周期)深度集成,提供了类型安全的异步编程体验。