Skip to content

Day 25: 异步编程入门

前面我们学习了多线程并发。多线程虽然强大,但有其局限性:创建线程有开销,线程间切换也有成本。对于IO密集型任务(如网络请求、文件读写),线程的效率不高。

异步编程(async/await)提供了一种更高效的并发模型:多个任务可以在少量线程上运行,当一个任务等待IO时,runtime切换到其他任务继续执行。

Rust的标准库提供了async/await语法和Future trait,但runtime需要借助第三方库来实现。今天我们学习async编程的基础知识。

async将一个函数变成返回Future的异步函数,await用于等待Future完成:

// 这是同步函数
fn sync_function() -> String {
String::from("同步结果")
}
// 这是异步函数,返回impl Future
async fn async_function() -> String {
String::from("异步结果")
}
fn main() {
// async函数返回的是一个Future,需要被执行器驱动
let future = async_function();
println!("创建了Future");
}

注意:async函数本身不会执行任何代码,它只是返回一个Future。只有当Future被poll(轮询)时,代码才会运行。

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用于任务唤醒。

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。

让我们看一个更有意义的例子,使用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),可以在多个任务间高效切换。

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个任务。

和线程类似,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的。

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的异步编程:

  1. async/.await 语法定义了异步函数和异步等待点
  2. Future trait 是异步计算的核心,需要被executor轮询执行
  3. tokio 是最流行的async runtime,提供了高效的task调度
  4. tokio::spawn 创建异步任务,比thread::spawn更轻量
  5. tokio::sync::channel 提供任务间通信

异步编程是现代高性能服务器和网络服务的基石。Rust的async设计与语言的其他特性(如所有权、生命周期)深度集成,提供了类型安全的异步编程体验。