Rust从入门到精通之进阶篇:15.异步编程

异步编程

异步编程是一种编程范式,允许程序在等待 I/O 操作完成时执行其他任务,而不是阻塞当前线程。Rust 的异步编程模型基于 Future 特质,结合 async/await 语法,提供了高效、安全的异步编程体验。在本章中,我们将探索 Rust 的异步编程模型和工具。

同步 vs 异步

在深入异步编程之前,让我们先了解同步和异步编程的区别:

同步编程

在同步编程模型中,当程序执行 I/O 操作(如读取文件或网络请求)时,当前线程会阻塞,直到操作完成:

rust 复制代码
use std::fs::File;
use std::io::Read;

fn read_file() -> std::io::Result<String> {
    let mut file = File::open("hello.txt")?; // 阻塞,直到文件打开
    let mut contents = String::new();
    file.read_to_string(&mut contents)?; // 阻塞,直到文件读取完成
    Ok(contents)
}

fn main() {
    // 这个调用会阻塞主线程,直到文件读取完成
    let contents = read_file().unwrap();
    println!("{}", contents);
}

异步编程

在异步编程模型中,I/O 操作不会阻塞当前线程,而是返回一个表示未来完成操作的值(在 Rust 中是 Future):

rust 复制代码
use tokio::fs::File;
use tokio::io::AsyncReadExt;

async fn read_file() -> std::io::Result<String> {
    let mut file = File::open("hello.txt").await?; // 不阻塞,返回 Future
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?; // 不阻塞,返回 Future
    Ok(contents)
}

#[tokio::main]
async fn main() {
    // 这个调用不会阻塞主线程
    let contents = read_file().await.unwrap();
    println!("{}", contents);
}

Future 特质

Rust 的异步编程基于 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),
    Pending,
}

Future 特质的核心是 poll 方法,它尝试解析 Future 的值。如果值已准备好,它返回 Poll::Ready(value);如果值尚未准备好,它返回 Poll::Pending 并安排在值准备好时再次调用 poll

手动实现 Future

虽然通常不需要手动实现 Future,但了解其工作原理很有帮助:

rust 复制代码
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};

struct Delay {
    when: Instant,
}

impl Future for Delay {
    type Output = ();
    
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if Instant::now() >= self.when {
            println!("Future 已完成");
            Poll::Ready(())
        } else {
            // 安排在未来某个时刻再次调用 poll
            let waker = cx.waker().clone();
            let when = self.when;
            
            std::thread::spawn(move || {
                let now = Instant::now();
                if now < when {
                    std::thread::sleep(when - now);
                }
                waker.wake();
            });
            
            println!("Future 尚未完成");
            Poll::Pending
        }
    }
}

#[tokio::main]
async fn main() {
    let delay = Delay {
        when: Instant::now() + Duration::from_secs(2),
    };
    
    println!("等待 Future 完成...");
    delay.await;
    println!("Future 已完成,程序继续执行");
}

async/await 语法

Rust 提供了 async/await 语法,简化了异步编程:

async 函数

async 关键字用于定义返回 Future 的函数:

rust 复制代码
async fn say_hello() {
    println!("Hello");
}

// 等价于:
fn say_hello() -> impl Future<Output = ()> {
    async {
        println!("Hello");
    }
}

await 表达式

.await 用于等待 Future 完成并获取其结果:

rust 复制代码
async fn get_user_id() -> u64 {
    // 模拟异步操作
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    42
}

async fn get_user_name(id: u64) -> String {
    // 模拟异步操作
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    format!("User {}", id)
}

#[tokio::main]
async fn main() {
    let id = get_user_id().await;
    let name = get_user_name(id).await;
    println!("{}", name);
}

async 块

async 也可以用于创建异步代码块:

rust 复制代码
#[tokio::main]
async fn main() {
    let future = async {
        let id = get_user_id().await;
        let name = get_user_name(id).await;
        name
    };
    
    let name = future.await;
    println!("{}", name);
}

异步运行时

Rust 的标准库提供了 Future 特质,但没有提供执行 Future 的运行时。为此,我们需要使用第三方库,如 Tokio、async-std 或 smol。

Tokio

Tokio 是 Rust 生态系统中最流行的异步运行时:

toml 复制代码
# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
rust 复制代码
use tokio::time::{sleep, Duration};

async fn task_one() {
    println!("任务一:开始");
    sleep(Duration::from_millis(100)).await;
    println!("任务一:完成");
}

async fn task_two() {
    println!("任务二:开始");
    sleep(Duration::from_millis(50)).await;
    println!("任务二:完成");
}

#[tokio::main]
async fn main() {
    // 串行执行
    task_one().await;
    task_two().await;
    
    println!("---");
    
    // 并发执行
    tokio::join!(
        task_one(),
        task_two()
    );
}

async-std

async-std 是另一个流行的异步运行时,API 设计与标准库类似:

toml 复制代码
# Cargo.toml
[dependencies]
async-std = { version = "1", features = ["attributes"] }
rust 复制代码
use async_std::task;
use std::time::Duration;

async fn task_one() {
    println!("任务一:开始");
    task::sleep(Duration::from_millis(100)).await;
    println!("任务一:完成");
}

async fn task_two() {
    println!("任务二:开始");
    task::sleep(Duration::from_millis(50)).await;
    println!("任务二:完成");
}

#[async_std::main]
async fn main() {
    // 串行执行
    task_one().await;
    task_two().await;
    
    println!("---");
    
    // 并发执行
    let t1 = task::spawn(task_one());
    let t2 = task::spawn(task_two());
    
    t1.await;
    t2.await;
}

异步并发工具

并发执行多个 Future

tokio::join!

tokio::join! 宏并发执行多个 Future,等待所有 Future 完成:

rust 复制代码
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let (result1, result2) = tokio::join!(
        async {
            sleep(Duration::from_millis(100)).await;
            "结果一"
        },
        async {
            sleep(Duration::from_millis(50)).await;
            "结果二"
        }
    );
    
    println!("{}, {}", result1, result2);
}
futures::join!

futures 库也提供了类似的 join! 宏:

rust 复制代码
use futures::join;
use async_std::task;
use std::time::Duration;

#[async_std::main]
async fn main() {
    let (result1, result2) = join!(
        async {
            task::sleep(Duration::from_millis(100)).await;
            "结果一"
        },
        async {
            task::sleep(Duration::from_millis(50)).await;
            "结果二"
        }
    );
    
    println!("{}, {}", result1, result2);
}

任务生成

tokio::spawn

tokio::spawn 在 Tokio 运行时中生成一个新任务:

rust 复制代码
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        sleep(Duration::from_secs(1)).await;
        "Hello from task"
    });
    
    // 做其他工作...
    
    let result = handle.await.unwrap();
    println!("{}", result);
}
async_std::task::spawn

async_std::task::spawn 在 async-std 运行时中生成一个新任务:

rust 复制代码
use async_std::task;
use std::time::Duration;

#[async_std::main]
async fn main() {
    let handle = task::spawn(async {
        task::sleep(Duration::from_secs(1)).await;
        "Hello from task"
    });
    
    // 做其他工作...
    
    let result = handle.await;
    println!("{}", result);
}

选择第一个完成的 Future

tokio::select!

tokio::select! 宏等待多个 Future 中的第一个完成:

rust 复制代码
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    tokio::select! {
        _ = sleep(Duration::from_secs(1)) => {
            println!("1 秒过去了");
        }
        _ = sleep(Duration::from_secs(2)) => {
            println!("2 秒过去了");
        }
    }
    // 输出:1 秒过去了
}
futures::select!

futures 库也提供了类似的 select! 宏:

rust 复制代码
use futures::select;
use futures::future::{self, FutureExt};
use async_std::task;
use std::time::Duration;

#[async_std::main]
async fn main() {
    let a = task::sleep(Duration::from_secs(1)).fuse();
    let b = task::sleep(Duration::from_secs(2)).fuse();
    
    select! {
        _ = a => println!("1 秒过去了"),
        _ = b => println!("2 秒过去了"),
    }
    // 输出:1 秒过去了
}

异步 I/O

文件操作

rust 复制代码
use tokio::fs::File;
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    // 异步写入文件
    let mut file = File::create("hello.txt").await?;
    file.write_all(b"Hello, world!").await?;
    
    // 异步读取文件
    let mut file = File::open("hello.txt").await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    
    println!("{}", contents);
    Ok(())
}

网络操作

rust 复制代码
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 服务器
    let server = tokio::spawn(async {
        let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
        let (mut socket, _) = listener.accept().await.unwrap();
        
        let mut buf = [0; 1024];
        let n = socket.read(&mut buf).await.unwrap();
        println!("服务器收到: {}", String::from_utf8_lossy(&buf[..n]));
        
        socket.write_all(b"Hello from server").await.unwrap();
    });
    
    // 客户端
    let client = tokio::spawn(async {
        let mut stream = TcpStream::connect("127.0.0.1:8080").await.unwrap();
        
        stream.write_all(b"Hello from client").await.unwrap();
        
        let mut buf = [0; 1024];
        let n = stream.read(&mut buf).await.unwrap();
        println!("客户端收到: {}", String::from_utf8_lossy(&buf[..n]));
    });
    
    // 等待两个任务完成
    tokio::try_join!(server, client)?;
    
    Ok(())
}

异步流(Stream)

Stream 特质类似于同步的 Iterator,但是异步产生值:

rust 复制代码
use futures::stream::{self, StreamExt};

#[tokio::main]
async fn main() {
    // 创建一个简单的流
    let mut stream = stream::iter(vec![1, 2, 3, 4, 5]);
    
    // 使用 next 方法异步迭代流
    while let Some(value) = stream.next().await {
        println!("{}", value);
    }
    
    // 使用组合器方法处理流
    let sum = stream::iter(vec![1, 2, 3, 4, 5])
        .fold(0, |acc, x| async move { acc + x })
        .await;
    
    println!("Sum: {}", sum);
}

实际示例:处理 TCP 连接流

rust 复制代码
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use futures::stream::StreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    let mut incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
    
    while let Some(socket_result) = incoming.next().await {
        let mut socket = socket_result?;
        
        // 为每个连接生成一个新任务
        tokio::spawn(async move {
            let mut buf = [0; 1024];
            
            loop {
                match socket.read(&mut buf).await {
                    Ok(0) => return, // 连接关闭
                    Ok(n) => {
                        // 回显收到的数据
                        if socket.write_all(&buf[..n]).await.is_err() {
                            return;
                        }
                    }
                    Err(_) => return,
                }
            }
        });
    }
    
    Ok(())
}

异步编程的挑战

错误处理

异步代码中的错误处理与同步代码类似,但需要注意 .await 的位置:

rust 复制代码
use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    // 方法 1:使用 ? 运算符
    async fn read_file() -> io::Result<String> {
        let mut file = File::open("hello.txt").await?;
        let mut contents = String::new();
        file.read_to_string(&mut contents).await?;
        Ok(contents)
    }
    
    // 方法 2:使用 match 表达式
    async fn read_file_alt() -> io::Result<String> {
        let file_result = File::open("hello.txt").await;
        let mut file = match file_result {
            Ok(file) => file,
            Err(e) => return Err(e),
        };
        
        let mut contents = String::new();
        match file.read_to_string(&mut contents).await {
            Ok(_) => Ok(contents),
            Err(e) => Err(e),
        }
    }
    
    let contents = read_file().await?;
    println!("{}", contents);
    
    Ok(())
}

异步上下文限制

在 Rust 中,不能在同步函数中直接使用 .await

rust 复制代码
// 错误:不能在同步函数中使用 .await
fn sync_function() {
    let future = async { "Hello" };
    let result = future.await; // 错误!
}

解决方案是使整个函数异步,或使用运行时的阻塞方法:

rust 复制代码
// 方法 1:使整个函数异步
async fn async_function() {
    let future = async { "Hello" };
    let result = future.await;
    println!("{}", result);
}

// 方法 2:使用运行时的阻塞方法
fn sync_function() {
    let future = async { "Hello" };
    let result = tokio::runtime::Runtime::new()
        .unwrap()
        .block_on(future);
    println!("{}", result);
}

生命周期问题

异步函数中的引用可能导致复杂的生命周期问题:

rust 复制代码
// 错误:返回的 Future 包含对 name 的引用,但 name 在函数返回时已经离开作用域
async fn process(name: &str) -> String {
    format!("处理: {}", name)
}

fn main() {
    let future;
    {
        let name = String::from("Alice");
        future = process(&name); // 错误:name 的生命周期不够长
    }
    // name 已经离开作用域,但 future 仍然持有对它的引用
}

解决方案是调整生命周期或使用所有权:

rust 复制代码
// 方法 1:调整生命周期
async fn process<'a>(name: &'a str) -> String {
    format!("处理: {}", name)
}

// 方法 2:使用所有权
async fn process_owned(name: String) -> String {
    format!("处理: {}", name)
}

fn main() {
    let name = String::from("Alice");
    let future = process_owned(name);
    // 现在 future 拥有 name 的所有权
}

异步编程最佳实践

1. 避免阻塞异步运行时

在异步函数中避免使用阻塞操作,如 std::thread::sleep 或同步 I/O:

rust 复制代码
// 不好的做法
async fn bad_practice() {
    // 这会阻塞整个异步运行时线程!
    std::thread::sleep(std::time::Duration::from_secs(1));
}

// 好的做法
async fn good_practice() {
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}

对于无法避免的阻塞操作,使用 spawn_blocking

rust 复制代码
#[tokio::main]
async fn main() {
    // 在单独的线程中执行阻塞操作
    let result = tokio::task::spawn_blocking(|| {
        // 这里可以执行阻塞操作
        std::thread::sleep(std::time::Duration::from_secs(1));
        "完成"
    }).await.unwrap();
    
    println!("{}", result);
}

2. 适当使用并发

使用 join!spawn 并发执行独立任务:

rust 复制代码
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 并发获取多个资源
    let (users, posts) = tokio::join!(
        fetch_users(),
        fetch_posts()
    );
    
    // 处理结果
    for user in users? {
        println!("用户: {}", user);
    }
    
    for post in posts? {
        println!("文章: {}", post);
    }
    
    Ok(())
}

async fn fetch_users() -> Result<Vec<String>, Box<dyn std::error::Error>> {
    // 模拟网络请求
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    Ok(vec![String::from("Alice"), String::from("Bob")])
}

async fn fetch_posts() -> Result<Vec<String>, Box<dyn std::error::Error>> {
    // 模拟网络请求
    tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
    Ok(vec![String::from("Post 1"), String::from("Post 2")])
}

3. 使用超时

为异步操作设置超时,避免无限等待:

rust 复制代码
use tokio::time::{timeout, Duration};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 设置 1 秒超时
    match timeout(Duration::from_secs(1), slow_operation()).await {
        Ok(result) => println!("操作完成: {}", result?),
        Err(_) => println!("操作超时"),
    }
    
    Ok(())
}

async fn slow_operation() -> Result<String, Box<dyn std::error::Error>> {
    // 模拟慢操作
    tokio::time::sleep(Duration::from_secs(2)).await;
    Ok(String::from("操作结果"))
}

4. 使用适当的错误处理

在异步代码中使用 ? 运算符和 Result 类型:

rust 复制代码
use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    let result = read_and_process_file("config.txt").await;
    
    match result {
        Ok(content) => println!("处理结果: {}", content),
        Err(e) => eprintln!("错误: {}", e),
    }
    
    Ok(())
}

async fn read_and_process_file(path: &str) -> io::Result<String> {
    let mut file = File::open(path).await
        .map_err(|e| io::Error::new(e.kind(), format!("无法打开文件 {}: {}", path, e)))?;
    
    let mut content = String::new();
    file.read_to_string(&mut content).await
        .map_err(|e| io::Error::new(e.kind(), format!("无法读取文件 {}: {}", path, e)))?;
    
    // 处理内容
    Ok(format!("处理后的内容: {}", content))
}

5. 使用 Stream 处理异步数据流

对于需要处理异步数据流的场景,使用 Stream 特质:

rust 复制代码
use futures::stream::{self, StreamExt};
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    // 创建一个异步数据流
    let mut stream = stream::unfold(0, |state| async move {
        if state < 5 {
            sleep(Duration::from_millis(100)).await;
            Some((state, state + 1))
        } else {
            None
        }
    });
    
    // 使用 for_each 并发处理流中的每个项
    stream.for_each_concurrent(2, |item| async move {
        println!("处理项: {}", item);
        sleep(Duration::from_millis(200)).await;
        println!("项 {} 处理完成", item);
    }).await;
}

练习题

  1. 创建一个异步函数,它接受一个文件路径,读取文件内容,并返回文件中的单词数量。使用 Tokio 运行时执行这个函数。

  2. 实现一个简单的异步 HTTP 客户端,它可以并发地从多个 URL 获取内容,并返回每个 URL 的内容长度。使用 reqwest 库和 tokio::join! 宏。

  3. 创建一个异步 TCP 回显服务器,它接受客户端连接,并将收到的所有数据回显给客户端。使用 Tokio 的 TcpListenerTcpStream

  4. 实现一个异步函数,它模拟一个可能失败的操作,并在失败时自动重试最多三次,每次重试之间有递增的延迟。使用 Tokio 的 sleep 函数。

  5. 创建一个异步

相关推荐
Asthenia04129 分钟前
操作系统入门:位示图、主存分配、页面置换与磁盘管理
后端
_丿丨丨_21 分钟前
PHP回调后门小总结
android·开发语言·php
想做富婆26 分钟前
Strawberry perl的下载,查询版本号,配置Path环境变量,查找perl解释器的位置
开发语言·perl
李是啥也不会31 分钟前
如何通过JavaScript实现点击播放音频
开发语言·javascript·音视频
加瓦点灯38 分钟前
外观模式(Facade Pattern):复杂系统的“统一入口”
后端
Asthenia04121 小时前
分页入门:简单分页与其他内存管理方式,操作系统小白指南
后端
Asthenia04121 小时前
Linux系统调用入门:进程(execve,exit,getpid,getpgid)/文件(open,close,read,write)
后端
Asthenia04121 小时前
从宏观到微观:MMU、PCB、TLB、CPU是个啥?
后端
Asthenia04121 小时前
操作系统期末复习:深入理解文件组织形式(连续/链接/索引)及Linux实际用法
后端
程序员老冯头1 小时前
第十一节 MATLAB关系运算符
开发语言·前端·数据结构·算法·matlab