Skip to main content

52 posts tagged with "Rust"

View All Tags

在现代编程中,异步编程已经成为提高程序性能和资源利用率的重要手段。 Rust作为一门系统级编程语言,提供了多种实现异步的方式。

本文将从概念入手,逐步深入探讨Rust中实现异步的各种方法,从标准库到第三方库, 全面介绍Rust的异步编程生态。

1. 异步编程的概念

异步编程是一种允许多个任务并发执行而不需要多线程的编程范式。

它特别适用于I/O密集型任务,可以在等待I/O操作完成时执行其他任务,从而提高程序的整体效率。

在Rust中,异步编程主要围绕Future特征(trait)展开。 Future代表一个可能还未完成的异步操作,它定义了异步任务的行为。

2. 使用标准库实现异步

2.1 手动实现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 = &'static str;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if Instant::now() >= self.when {
println!("Future completed");
Poll::Ready("done")
} else {
println!("Future not ready yet");
cx.waker().wake_by_ref();
Poll::Pending
}
}
}

#[tokio::main]
async fn main() {
let future = Delay {
when: Instant::now() + Duration::from_secs(2),
};

println!("Waiting...");
let out = future.await;
println!("Future returned: {}", out);
}

适用场景

  • 需要对异步操作有精细控制的场景
  • 实现自定义的复杂异步原语
  • 学习和理解Rust异步机制的底层原理

手动实现Future

2.2 使用async/await语法

Rust提供了async/await语法糖,使得编写异步代码变得更加简单和直观。

use tokio::time::{sleep, Duration};

async fn fetch_data(id: u32) -> String {
sleep(Duration::from_millis(100)).await;
format!("Data for id {}", id)
}

#[tokio::main]
async fn main() {
let data1 = fetch_data(1).await;
println!("Fetched: {}", data1);

let results = tokio::join!(
fetch_data(2),
fetch_data(3),
fetch_data(4)
);

println!("Fetched: {:?}", results);
}

适用场景

  • 大多数异步编程场景
  • 需要简洁、易读的异步代码
  • 处理复杂的异步流程,如并发操作

async/await异步

3. 使用第三方库实现异步

3.1 Tokio

Tokio是Rust最流行的异步运行时之一,它提供了全面的异步编程支持。

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("Server listening on port 8080");

loop {
let (mut socket, _) = listener.accept().await?;

tokio::spawn(async move {
let mut buf = [0; 1024];

loop {
let n = match socket.read(&mut buf).await {
Ok(n) if n == 0 => return,
Ok(n) => n,
Err(_) => return,
};

if let Err(e) = socket.write_all(&buf[0..n]).await {
eprintln!("failed to write to socket; err = {:?}", e);
return;
}
}
});
}
}

适用场景

  • 构建高性能的网络应用
  • 需要完整的异步生态系统支持
  • 大型项目,需要丰富的异步工具和抽象

Tokio异步

3.2 async-std

async-std是另一个流行的异步运行时,它的API设计更接近Rust标准库。

use async_std::net::TcpListener;
use async_std::prelude::*;
use async_std::task;
use futures::stream::StreamExt;

async fn handle_client(mut stream: async_std::net::TcpStream) -> std::io::Result<()> {
println!("Accepted connection from: {}", stream.peer_addr()?);
let mut buffer = [0; 1024];
while let Ok(n) = stream.read(&mut buffer).await {
if n == 0 { return Ok(()) }
stream.write_all(&buffer[0..n]).await?;
}
Ok(())
}

#[async_std::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("Server listening on port 8080");

listener
.incoming()
.for_each_concurrent(None, |stream| async move {
let stream = stream.unwrap();
task::spawn(handle_client(stream));
})
.await;

Ok(())
}

适用场景

  • 需要与标准库API相似的异步接口
  • 中小型项目,追求简洁性
  • 学习异步编程,过渡到异步代码

async-std异步

4. 高级异步模式

4.1 使用futures库

futures库提供了更多用于组合和操作Future的工具。

use futures::future::{join_all, FutureExt};
use reqwest;
use std::time::Instant;

async fn fetch_url(url: &str) -> Result<(String, String), reqwest::Error> {
let resp = reqwest::get(url).await?;
let body = resp.text().await?;
Ok((url.to_string(), body))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let urls = vec![
"https://www.rust-lang.org",
"https://github.com/rust-lang/rust",
"https://crates.io",
];

let start = Instant::now();
let futures = urls.into_iter().map(|url| fetch_url(url).boxed());
let results = join_all(futures).await;

for result in results {
match result {
Ok((url, body)) => println!("URL: {}, length: {} bytes", url, body.len()),
Err(e) => eprintln!("Error: {}", e),
}
}

println!("Total time: {:?}", start.elapsed());

Ok(())
}

适用场景

  • 需要复杂的Future组合和操作
  • 处理大量并发异步任务
  • 实现自定义的异步控制流

futures异步

5. 深入理解:事件循环的工作原理

在讨论了各种异步实现方式后,让我们深入了解Rust异步系统的核心:事件循环。

事件循环是异步运行时(如Tokio或async-std)的核心组件,它负责管理和执行异步任务。 理解事件循环的工作原理对于掌握Rust的异步编程至关重要。

事件循环的基本流程

  1. 调用 poll:事件循环会反复调用每个Futurepoll方法,以检查其状态。 如果任务能够推进,poll会执行一部分操作,但如果任务需要等待外部事件(例如 I/O 完成),它就会返回Pending状态。
  2. 挂起等待(sleep:当poll返回Pending时,任务会挂起,并不会占用 CPU。 这时,事件循环可以继续处理其他任务,而挂起的任务会注册一个Waker
  3. 唤醒(wake:一旦外部事件完成,Waker会被触发,唤醒等待的任务,将其标记为"可执行"状态。 然后,事件循环会重新调度该任务。
  4. 继续 poll:被唤醒的任务将再次由事件循环调用其poll方法,从挂起的位置继续执行。 这种机制会一直进行,直到任务完成或再次需要等待其他事件。

这个过程的核心是事件循环不断调用 poll,在适当时机挂起任务和唤醒任务。 Rust 的异步系统基于这种高效的任务调度,可以在单线程上处理大量并发任务,同时节省资源。

事件循环

深入解析

  1. 高效的资源利用

    • 当一个任务在等待I/O或其他外部事件时,它不会阻塞整个线程。
    • 事件循环可以继续执行其他准备就绪的任务,从而最大化CPU利用率。
  2. 非阻塞I/O

    • Rust的异步模型依赖于非阻塞I/O操作。
    • 当I/O操作无法立即完成时,任务会返回Pending状态,而不是阻塞线程。
  3. Waker机制

    • Waker是Rust异步模型的关键创新。
    • 它允许任务在准备好继续执行时通知事件循环,而无需持续轮询。
  4. 零成本抽象

    • Rust的异步模型被设计为"零成本抽象",意味着你只为你使用的功能付出代价。
    • 编译器会将async/await语法转换为状态机,无需额外的运行时开销。
  5. 可组合性

    • Futures可以轻松组合,创建复杂的异步流程。
    • 这种组合性使得构建复杂的异步系统变得更加简单和直观。

实际应用示例

让我们通过一个简单的例子来说明事件循环的工作原理:

use tokio::time::{sleep, Duration};

async fn task1() {
println!("Task 1 starting");
sleep(Duration::from_secs(2)).await;
println!("Task 1 completed");
}

async fn task2() {
println!("Task 2 starting");
sleep(Duration::from_secs(1)).await;
println!("Task 2 completed");
}

#[tokio::main]
async fn main() {
tokio::join!(task1(), task2());
}

在这个例子中:

  1. 事件循环首先调用task1poll方法。task1开始执行,打印开始消息,然后遇到sleep操作。
  2. task1返回Pending状态,事件循环转而执行task2
  3. task2同样开始执行,打印开始消息,然后也遇到sleep操作。
  4. 此时两个任务都处于挂起状态,事件循环等待。
  5. 1秒后,task2的定时器触发,唤醒task2
  6. 事件循环再次poll task2,它完成执行并打印完成消息。
  7. 又过1秒后,task1的定时器触发,唤醒task1
  8. 事件循环poll task1,它完成执行并打印完成消息。

这个过程展示了事件循环如何有效地管理多个异步任务,即使在单线程环境中也能实现并发执行。

理解事件循环的工作原理是掌握Rust异步编程的关键。 它不仅帮助我们更好地理解异步代码的行为,还能指导我们编写更高效、更可靠的异步程序。 无论是使用标准库的Future,还是借助Tokio或async-std这样的异步运行时,核心原理都是一致的。 通过合理利用这些机制,我们可以充分发挥Rust在并发和异步编程方面的强大能力。

总结

Rust提供了丰富的异步编程选项,从底层的Future实现到高级的异步运行时和工具库。

通过这些例子和流程图,我们可以看到:

  1. 手动实现Future让我们深入理解了异步操作的本质,适合需要精细控制的场景。
  2. async/await语法大大简化了异步代码的编写,适合大多数日常异步编程任务。
  3. Tokio和async-std等运行时提供了强大的异步I/O支持,适合构建高性能的网络应用。
  4. futures库提供了更多高级的异步操作工具,适合复杂的异步流程控制。

选择合适的异步方式取决于你的具体需求:

  • 对于简单的异步任务,使用async/await语法就足够了。
  • 对于需要更细粒度控制的场景,可以考虑手动实现Future
  • 对于大型项目或需要全面异步支持的场景,Tokio或async-std这样的异步运行时是很好的选择。
  • 对于特定的并发模式或复杂的异步流程,futures库提供了强大的工具。

无论选择哪种方式,Rust的类型系统和所有权模型都能确保异步代码的安全性和高效性。

随着对异步编程的深入理解和实践,你将能够充分利用Rust强大的异步能力,编写出高性能、可靠的异步程序。

鱼雪

引言

在前两篇文章中,我们分别介绍了 Rust 异步编程的基石 - Future 和任务(Task)。

本篇文章我们将进一步探索异步 IO,这也是异步编程最常见的应用场景。

想象一下,当你在餐厅点餐时:

  • 同步模式就像是你点完餐后一直在座位上等待,直到服务员把餐品送到
  • 异步模式则是点完餐后你可以刷手机、看书,等餐品好了服务员会来通知你

这就是异步 IO 的核心思想 - 在等待 IO 操作完成时,我们的程序可以去做其他事情,而不是傻等着。

目录

  1. 从同步到异步
  2. 非阻塞 IO 基础
  3. 实现异步 IO
  4. 事件驱动机制
  5. 完整示例
  6. 最佳实践
  7. 总结

从同步到异步

传统的同步服务器

让我们先看一个最基础的 TCP 服务器:

fn main() -> io::Result<()> {
let listener = TcpListener::bind("0.0.0.0:8000")?;
let mut n = 1;

loop {
let (mut socket, _) = listener.accept()?;
socket.write_all(format!("start {}\n", n).as_bytes())?;
thread::sleep(Duration::from_secs(1));
socket.write_all(format!("end {}\n", n).as_bytes())?;
n += 1;
}
}

这个服务器做了什么?

  1. 监听 8000 端口
  2. 接受连接后发送一个开始消息
  3. 等待 1 秒
  4. 发送结束消息
  5. 继续等待下一个连接

看起来很简单对吧?但这里有个严重的问题 - 它一次只能处理一个连接。 当一个客户端连接时,其他客户端必须等待,这就是同步 IO 的局限。

多线程方案

一个直观的改进是使用多线程:

fn main() -> io::Result<()> {
let listener = TcpListener::bind("0.0.0.0:8000")?;
let mut n = 1;

loop {
let (socket, _) = listener.accept()?;
let n = n;
thread::spawn(move || {
handle_connection(socket, n)
});
n += 1;
}
}

这样每个连接都有自己的线程,可以并发处理多个连接。

但线程也有问题:

  • 创建和切换线程的开销较大
  • 每个线程都占用一定内存
  • 线程数量受系统限制

这就是为什么我们需要异步 IO。

非阻塞 IO 基础

在实现异步 IO 之前,我们需要理解几个重要概念:

阻塞与非阻塞

想象你去取快递:

  • 阻塞模式:你在快递点一直等到快递到达
  • 非阻塞模式:你留下电话,快递到了给你打电话

在代码中:

// 阻塞模式
let data = socket.read()?; // 会一直等待直到有数据

// 非阻塞模式
socket.set_nonblocking(true)?;
match socket.read() {
Ok(data) => { /* 处理数据 */ }
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
// 现在没有数据,稍后再试
}
Err(e) => return Err(e)
}

Ready 与 Pending

在异步编程中,我们用 ReadyPending 两个状态来表示操作是否完成:

enum Poll<T> {
Ready(T), // 操作完成,这里是结果
Pending, // 操作未完成,需要等待
}

实现异步 IO

现在我们来实现真正的异步 IO。

我们将分三步:

1. 异步 Accept

首先实现异步接受连接:

async fn accept(listener: &mut TcpListener) -> io::Result<(TcpStream, SocketAddr)> {
poll_fn(|cx| {
match listener.accept() {
Ok((stream, addr)) => {
stream.set_nonblocking(true)?;
Poll::Ready(Ok((stream, addr)))
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
// 注册唤醒器
register_waker(cx.waker(), listener);
Poll::Pending
}
Err(e) => Poll::Ready(Err(e))
}
}).await
}

这里的关键点是:

  1. 使用 poll_fn 创建 Future
  2. 处理 WouldBlock 错误
  3. 注册唤醒器

好的,我继续完成剩余部分:

2. 异步写入

接下来实现异步写入数据:

async fn write_all(buf: &[u8], stream: &mut TcpStream) -> io::Result<()> {
let mut remaining = buf;

poll_fn(|cx| {
while !remaining.is_empty() {
match stream.write(remaining) {
Ok(0) => {
return Poll::Ready(Err(
io::Error::new(io::ErrorKind::WriteZero, "写入零字节")
));
}
Ok(n) => {
remaining = &remaining[n..];
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
// 注册写就绪事件
register_write_interest(cx.waker(), stream);
return Poll::Pending;
}
Err(e) => return Poll::Ready(Err(e)),
}
}
Poll::Ready(Ok(()))
}).await
}

这个实现有几个重要特点:

  1. 使用循环确保所有数据都写入
  2. 处理写入零字节的错误情况
  3. 支持部分写入

3. 异步读取

最后是异步读取实现:

async fn read_to_string(stream: &mut TcpStream) -> io::Result<String> {
let mut buffer = String::new();

poll_fn(|cx| {
loop {
let mut chunk = [0u8; 1024];
match stream.read(&mut chunk) {
Ok(0) => return Poll::Ready(Ok(buffer)), // EOF
Ok(n) => {
if let Ok(s) = String::from_utf8_lossy(&chunk[..n]).into_owned() {
buffer.push_str(&s);
}
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
// 注册读就绪事件
register_read_interest(cx.waker(), stream);
return Poll::Pending;
}
Err(e) => return Poll::Ready(Err(e)),
}
}
}).await
}

事件驱动机制

事件循环

异步 IO 的核心是事件循环(Event Loop):

struct EventLoop {
poll: Poll,
events: Events,
tasks: HashMap<Token, Task>,
}

impl EventLoop {
fn run(&mut self) {
loop {
// 1. 等待事件
self.poll.poll(&mut self.events, Some(Duration::from_secs(1)))?;

// 2. 处理就绪的事件
for event in &self.events {
if let Some(task) = self.tasks.get_mut(&event.token()) {
task.wake();
}
}

// 3. 执行可运行的任务
self.run_ready_tasks();
}
}
}

事件循环的工作流程:

  1. 等待 IO 事件
  2. 唤醒对应的任务
  3. 执行就绪的任务
  4. 重复以上步骤

注册事件监听

fn register_interest(
registry: &Registry,
stream: &TcpStream,
token: Token,
interests: Interest,
) -> io::Result<()> {
registry.register(
&mut SourceFd(&stream.as_raw_fd()),
token,
interests,
)
}

完整示例

让我们看一个完整的异步 echo 服务器示例:

async fn handle_connection(mut socket: TcpStream) {
let mut buffer = [0; 1024];

loop {
match read(&mut socket, &mut buffer).await {
Ok(0) => break, // 连接关闭
Ok(n) => {
// Echo 收到的数据
if let Err(e) = write_all(&mut socket, &buffer[..n]).await {
eprintln!("写入错误: {}", e);
break;
}
}
Err(e) => {
eprintln!("读取错误: {}", e);
break;
}
}
}
}

#[tokio::main]
async fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("服务器运行在 127.0.0.1:8080");

loop {
let (socket, addr) = listener.accept().await?;
println!("新连接: {}", addr);

tokio::spawn(async move {
handle_connection(socket).await
});
}
}

最佳实践

1. 合理的超时处理

async fn with_timeout<F, T>(future: F, duration: Duration) -> io::Result<T>
where
F: Future<Output = io::Result<T>>,
{
match tokio::time::timeout(duration, future).await {
Ok(result) => result,
Err(_) => Err(io::Error::new(
io::ErrorKind::TimedOut,
"操作超时"
))
}
}

2. 错误处理

#[derive(Debug, Error)]
enum IoError {
#[error("连接断开: {0}")]
Disconnected(String),

#[error("超时: {0}")]
Timeout(String),

#[error("IO错误: {0}")]
Io(#[from] std::io::Error),
}

3. 资源管理

struct Connection {
stream: TcpStream,
timeout: Duration,
buffer_size: usize,
}

impl Connection {
async fn with_timeout<F, T>(&mut self, f: F) -> Result<T, IoError>
where
F: Future<Output = Result<T, IoError>>,
{
tokio::time::timeout(self.timeout, f)
.await
.map_err(|_| IoError::Timeout("操作超时".into()))?
}
}

4. 优雅关闭

async fn shutdown(listener: TcpListener, connections: Arc<Mutex<Vec<Connection>>>) {
// 1. 停止接受新连接
drop(listener);

// 2. 等待现有连接完成
let mut connections = connections.lock().await;
for conn in connections.drain(..) {
conn.shutdown().await;
}
}

性能优化

1. 使用缓冲区

struct BufferedStream {
inner: TcpStream,
read_buf: Vec<u8>,
write_buf: Vec<u8>,
}

impl BufferedStream {
fn with_capacity(stream: TcpStream, capacity: usize) -> Self {
Self {
inner: stream,
read_buf: Vec::with_capacity(capacity),
write_buf: Vec::with_capacity(capacity),
}
}
}

2. 批量处理

async fn batch_write<I>(stream: &mut TcpStream, items: I) -> io::Result<()>
where
I: IntoIterator,
I::Item: AsRef<[u8]>,
{
let mut buffer = Vec::new();

// 收集数据
for item in items {
buffer.extend_from_slice(item.as_ref());
}

// 一次性写入
write_all(stream, &buffer).await
}

3. 连接池

struct ConnectionPool {
idle: Vec<Connection>,
max_size: usize,
}

impl ConnectionPool {
async fn get(&mut self) -> io::Result<Connection> {
if let Some(conn) = self.idle.pop() {
Ok(conn)
} else if self.idle.len() < self.max_size {
Connection::new().await
} else {
// 等待空闲连接
self.wait_for_idle().await
}
}
}

总结

异步 IO 是 Rust 异步编程的核心应用场景之一。

通过本文我们了解到:

  1. 基础概念

    • 阻塞与非阻塞
    • ReadyPending
    • 事件驱动模型
  2. 实现技巧

    • 异步 Accept/Read/Write
    • 事件循环
    • 资源管理
  3. 最佳实践

    • 超时处理
    • 错误处理
    • 性能优化
    • 优雅关闭
  4. 注意事项

    • 合理使用缓冲区
    • 批量处理提升性能
    • 资源池化重用连接

下一步学习

  1. 深入了解 tokio 运行时
  2. 学习 async-std 库
  3. 探索 futures 库的高级特性
  4. 实践异步 Web 框架

参考资料

  1. Rust 异步编程文档
  2. Tokio 教程
  3. async-std 文档
  4. futures 库文档

附录: 常见问题解答

Q1: 什么时候该用异步 IO? A1: 当你的应用需要处理大量并发连接,而且这些连接大多数时间都在等待 IO 时。

Q2: 异步 IO 的主要优势是什么? A2: 可以用少量系统线程处理大量并发连接,降低系统资源消耗。

Q3: 异步 IO 的缺点是什么? A3: 代码复杂度增加,调试难度加大,需要特殊的错误处理机制。

鱼雪

目录

引言

上下文回顾

在上一篇文章中,我们深入探讨了 Rust 中 Future 的概念和实现原理。 我们了解到 Future 是异步编程的基础抽象,它代表了一个可能在未来完成的值。

任务基础

关键概念速览

在深入细节之前,让我们先快速了解本文将要讨论的核心概念:

概念描述主要作用
Future代表未来可能的值定义异步操作
TaskFuture 的运行时实例管理异步操作的执行
Spawn任务创建机制提交任务到执行器
JoinHandle任务控制句柄等待和管理任务结果
Waker任务唤醒机制通知执行器任务可继续执行

快速上手示例

use tokio;
use std::time::Duration;

#[tokio::main]
async fn main() {
// 1. 创建一个简单的异步任务
let handle = tokio::spawn(async {
println!("Task started");
tokio::time::sleep(Duration::from_secs(1)).await;
println!("Task completed");
"Task result"
});

// 2. 等待任务完成
let result = handle.await.unwrap();
println!("Got result: {}", result);
}

任务的定义与作用

任务(Task)是异步运行时中的最小执行单位,它封装了一个 Future 实例及其执行上下文。

每个任务代表一个独立的异步操作流程。

  1. Future 的定义
// 当你定义一个异步函数或块时,实际上是定义了一个 Future
async fn my_async_function() -> Result<String, Error> {
// 这个函数会返回一个实现了 Future trait 的类型
// 而不是直接返回 Result<String, Error>
}

// 或者使用 async 块
let future = async {
// 这里的代码定义了 Future 的行为
};
  1. Task 的创建
// 当你使用 spawn 或类似的方法来执行 Future 时,运行时会将 Future 封装为 Task
tokio::spawn(my_async_function()); // Future 被转换为 Task 并提交给执行器

让我用一个类比来说明:

  1. Future 就像是一个"食谱"

    • 描述了"要做什么"
    • 包含了所有必要的步骤
    • 但还没有真正开始执行
  2. Task 就像是"正在烹饪的过程"

    • Future 的运行时实例
    • 包含了实际执行的状态
    • 有自己的资源和上下文
  3. 任务与 Future 的关系 任务是 Future 的运行时表示:

  • Future 定义了异步操作的逻辑
  • Task 负责管理 Future 的执行状态
  • Task 处理与执行器的交互

让我画个简单的图来说明这个关系:

Rust Future与Task的关系

关键区别:

  1. Future(特征/定义)

    • 是一个特征trait
    • 定义了异步计算的逻辑
    • 是静态的定义
    • 可以被多次执行
    • 不包含执行状态
  2. Task(运行时实例)

    • Future 的运行时表示
    • 包含执行状态和上下文
    • 是动态的实例
    • 有自己的生命周期
    • 包含调度信息

实际使用示例:

// 1. 定义 Future
async fn fetch_data(url: String) -> Result<String, Error> {
// 异步操作的定义
let response = reqwest::get(&url).await?;
let text = response.text().await?;
Ok(text)
}

// 2. 创建多个 Task
async fn main() {
// 同一个 Future 定义可以创建多个不同的 Task
let task1 = tokio::spawn(fetch_data("url1".to_string()));
let task2 = tokio::spawn(fetch_data("url2".to_string()));

// 等待所有 Task 完成
let (result1, result2) = join!(task1, task2);
}

这种设计的优点:

  1. 灵活性:同一个 Future 可以被多次执行
  2. 资源管理Task 可以独立管理资源
  3. 并发控制:执行器可以有效调度多个 Task
  4. 状态隔离:每个 Task 有自己的执行状态

理解这个关系对于编写高效的异步代码很重要,因为它帮助我们:

  • 更好地组织异步代码结构
  • 理解执行流程
  • 处理并发和资源管理
  • 优化性能

任务的生命周期

任务从创建到完成经历以下阶段:

Rust 异步

  1. 创建阶段:任务被构造并初始化
  2. 调度阶段:任务被提交到执行器
  3. 执行阶段:任务被轮询(poll)执行
  4. 等待阶段:任务等待资源或事件
  5. 完成阶段:任务执行完成或失败

任务的核心组件

动态分发(Dyn

为什么需要动态分发

在异步运行时中,我们需要管理不同类型的 Future

动态分发允许我们用统一的方式处理这些 Future

// 动态 Future 类型定义
type DynFuture = Pin<Box<dyn Future<Output = ()> + Send>>;

// 使用示例
fn store_future<F>(future: F)
where
F: Future<Output = ()> + Send + 'static
{
let boxed: DynFuture = Box::pin(future);
// 存储或处理 boxed future
}

性能考虑

动态分发虽然提供了灵活性,但也带来了一些开销:

  • 额外的内存分配(Box
  • 虚表查找的开销
  • 潜在的缓存未命中

Spawn 机制

基本概念

spawn 是向执行器提交任务的标准方式:

pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
// 创建任务并返回句柄
let (handle, task) = create_task(future);

// 提交任务到执行器
EXECUTOR.submit(task);

handle
}

实现细节

高效的任务生成机制需要考虑:

  • 任务状态管理
  • 资源分配策略
  • 错误处理机制

JoinHandle

设计思想

JoinHandle 提供了等待任务完成的机制:

pub struct JoinHandle<T> {
state: Arc<Mutex<JoinState<T>>>,
}

enum JoinState<T> {
Running(Waker),
Completed(T),
Failed(Error),
}

impl<T> Future for JoinHandle<T> {
type Output = Result<T, Error>;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut state = self.state.lock().unwrap();
match &*state {
JoinState::Completed(value) => Poll::Ready(Ok(value.clone())),
JoinState::Failed(error) => Poll::Ready(Err(error.clone())),
JoinState::Running(_) => {
*state = JoinState::Running(cx.waker().clone());
Poll::Pending
}
}
}
}

唤醒机制

Waker 的作用

Waker 负责在任务可继续执行时通知执行器:

pub struct CustomWaker {
task_id: TaskId,
task_queue: Arc<Mutex<TaskQueue>>,
}

impl Wake for CustomWaker {
fn wake(self: Arc<Self>) {
let mut queue = self.task_queue.lock().unwrap();
queue.push(self.task_id);
}
}

实践示例

基础使用模式

#[tokio::main]
async fn main() {
// 创建多个任务
let mut handles = Vec::new();

for i in 0..5 {
let handle = tokio::spawn(async move {
println!("Task {} started", i);
tokio::time::sleep(Duration::from_secs(1)).await;
println!("Task {} completed", i);
i
});
handles.push(handle);
}

// 等待所有任务完成
for handle in handles {
let result = handle.await.unwrap();
println!("Got result: {}", result);
}
}

高级应用场景

任务取消

async fn cancellable_task(cancel: CancellationToken) -> Result<(), Error> {
loop {
tokio::select! {
_ = cancel.cancelled() => {
println!("Task cancelled");
return Ok(());
}
_ = async_operation() => {
println!("Operation completed");
}
}
}
}

最佳实践与性能优化

任务粒度

  • 避免过细的任务粒度
  • 合理批处理小任务
  • 控制任务数量

批处理优化示例

use futures::stream::{self, StreamExt};
use tokio::task;

async fn process_items_batched(items: Vec<i32>) -> Result<(), Error> {
// 将items分批处理
let batch_size = 100;
let mut batches = stream::iter(items)
.chunks(batch_size)
.map(|chunk| {
task::spawn(async move {
for item in chunk {
process_single_item(item).await?;
}
Ok::<_, Error>(())
})
})
.buffer_unwind(10); // 控制并发数量

while let Some(result) = batches.next().await {
result??; // 处理错误
}

Ok(())
}

// 对比:细粒度任务版本
async fn process_items_fine_grained(items: Vec<i32>) -> Result<(), Error> {
let handles: Vec<_> = items
.into_iter()
.map(|item| {
task::spawn(async move {
process_single_item(item).await
})
})
.collect();

for handle in handles {
handle.await??;
}

Ok(())
}

资源管理

  • 使用资源池
  • 实现超时机制
  • 处理任务泄漏

资源限制

use tokio::sync::Semaphore;

async fn with_limit<F, T>(
sem: Arc<Semaphore>,
task: F
) -> Result<T, Error>
where
F: Future<Output = Result<T, Error>>,
{
let _permit = sem.acquire().await?;
task.await
}

连接池管理

use bb8::Pool;

async fn create_pool() -> Pool<MyConnectionManager> {
let manager = MyConnectionManager::new("connection_string");
Pool::builder()
.max_size(15)
.min_idle(Some(5))
.build(manager)
.await
.unwrap()
}

超时控制

async fn with_timeout<F, T>(future: F, duration: Duration) -> Result<T, TimeoutError>
where
F: Future<Output = T>,
{
timeout(duration, future).await
}

错误处理

  • 实现优雅降级
  • 添加重试机制
  • 日志记录

健壮的错误处理模式

use tokio::time::timeout;
use backoff::{ExponentialBackoff, backoff::Backoff};

async fn robust_task() -> Result<(), Error> {
// 1. 超时处理
let operation_result = timeout(
Duration::from_secs(5),
async {
// 你的异步操作
Ok::<_, Error>(())
}
).await??;

// 2. 重试机制
let mut backoff = ExponentialBackoff::default();
let retry_result = async {
loop {
match async_operation().await {
Ok(value) => break Ok(value),
Err(e) => {
if let Some(duration) = backoff.next_backoff() {
tokio::time::sleep(duration).await;
continue;
}
break Err(e);
}
}
}
}.await?;

// 3. 资源清理
struct CleanupGuard<T>(T);
impl<T> Drop for CleanupGuard<T> {
fn drop(&mut self) {
// 清理资源
}
}

let _guard = CleanupGuard(resource);

Ok(())
}

任务粒度优化

以下是不同任务粒度的性能对比:

策略优点缺点适用场景
细粒度更好的响应性更高的调度开销IO密集型,需要快速响应
批处理更低的开销延迟可能增加CPU密集型,吞吐量优先
混合模式平衡的性能实现复杂复杂业务场景

参考资料

  1. Rust 异步编程文档:async-book
  2. [Tokio 文档:tokio.rs]](https://tokio.rs/)
  3. Rust RFC 2592:futures-api-v0.3
鱼雪

目录

  1. 引言
  2. 派生宏基础
  3. 创建自定义派生宏
  4. 生产环境中的派生宏应用
  5. 高级主题
  6. 测试与调试
  7. 总结与展望

引言

派生宏(Derive Macros)是Rust中的一种强大的元编程工具, 它允许我们通过注解的方式自动为类型实现特定的trait。

通过使用#[derive(...)]属性,我们可以避免编写大量的样板代码,提高开发效率。

为什么需要派生宏?

// 不使用派生宏
struct Point {
x: i32,
y: i32,
}

impl std::fmt::Debug for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}

// 使用派生宏
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}

派生宏的优势:

  • 减少重复代码
  • 提高代码可维护性
  • 降低出错可能性
  • 提升开发效率

派生宏基础

工作原理

派生宏在编译时展开,生成实现特定trait的代码。

它们是过程宏的一种,可以访问和操作Rust的抽象语法树(AST)。

常见的标准库派生宏

  1. 基础trait
#[derive(Debug, Clone, Copy)]
struct Vector2D {
x: f64,
y: f64,
}
  1. 比较相关
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Version(u32, u32, u32);
  1. 数据处理
#[derive(Serialize, Deserialize)]
struct Config {
#[serde(default = "default_port")]
port: u16,
#[serde(rename = "host_name")]
host: String,
}

语法规则

  1. 基本语法
#[derive(TraitName1, TraitName2)]
struct MyStruct {
// fields...
}
  1. 带属性参数
#[derive(Builder)]
#[builder(setter(into))]
struct Command {
#[builder(default = "\"localhost\".to_string()")]
host: String,
#[builder(default = "8080")]
port: u16,
}

创建自定义派生宏

基本步骤

  1. 创建过程宏项目
[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"
  1. 实现派生宏
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloWorld)]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;

let expanded = quote! {
impl #name {
fn hello_world() {
println!("Hello, World! I'm {}", stringify!(#name));
}
}
};

TokenStream::from(expanded)
}

这个宏实现了给结构体添加一个hello_world方法。

工具链介绍

  1. syn: 解析Rust代码为语法树
  2. quote: 将语法树转换回Rust代码
  3. proc-macro2: 提供底层Token处理功能

生产环境中的派生宏应用

常见使用场景

  1. 序列化/反序列化
#[derive(Serialize, Deserialize)]
struct User {
id: u64,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
}
  1. 错误处理
#[derive(Error, Debug)]
pub enum ApiError {
#[error("请求失败: {0}")]
RequestFailed(#[from] reqwest::Error),

#[error("数据库错误: {0}")]
DatabaseError(#[from] sqlx::Error),
}
  1. 命令行参数解析
#[derive(Parser)]
#[clap(version = "1.0", author = "Your Name")]
struct Opts {
#[clap(short, long)]
config: PathBuf,

#[clap(short, long, default_value = "info")]
log_level: String,
}

流行的派生宏库

  1. serde: 序列化框架
  2. thiserror: 错误处理
  3. clap: 命令行参数解析
  4. async-trait: 异步trait支持
  5. derive_more: 通用派生宏集合

最佳实践

  1. 性能考虑
// 避免不必要的Clone实现
#[derive(Debug, Copy)] // 优先使用Copy而不是Clone
struct SmallType {
x: i32,
y: i32,
}
  1. 属性组织
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
struct ApiResponse {
status_code: u16,
message: String,
}

高级主题

条件派生

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
struct Configuration {
name: String,
value: i32,
}

如果featureserde,则生成SerializeDeserialize的实现。

自定义错误处理

#[derive(Error, Debug)]
pub enum CustomError {
#[error("验证失败: {field} - {message}")]
ValidationError {
field: String,
message: String,
},

#[error(transparent)]
Other(#[from] anyhow::Error),
}

实现了Errortrait,并添加了anyhow::Error的转换。

性能优化

  1. 编译时优化
// 使用 Box 减少编译时内存使用
#[derive(Debug)]
struct LargeStruct {
#[debug(skip)]
large_data: Box<[u8]>,
metadata: String,
}

测试与调试

单元测试

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_derive_debug() {
#[derive(Debug)]
struct Test {
field: i32,
}

let instance = Test { field: 42 };
assert_eq!(format!("{:?}", instance), "Test { field: 42 }");
}
}

调试技巧

  1. 使用cargo expand查看宏展开
  2. 使用println!在编译时打印信息
  3. 使用cargo-expand查看完整的展开代码

总结与展望

派生宏是Rust中强大的代码生成工具,能够:

  • 减少重复代码
  • 提高开发效率
  • 保证实现的正确性
  • 提供良好的抽象

未来发展方向:

  • 更强大的编译时类型检查
  • 更好的错误提示
  • 更多的标准库支持
  • 更完善的IDE支持

参考资料

  1. Rust官方文档 - 派生宏
  2. syn文档
  3. quote文档
  4. The Rust Reference - Procedural Macros
  5. Rust设计模式 - 派生宏模式
鱼雪

Rust 异步Future

目录

  1. 引言
  2. Futures概述
    • 什么是Futures
    • Futures的基本组成
  3. 代码示例解析
    • foo函数的异步实现
    • foo函数的同步实现
  4. JoinAll的实现
  5. 自定义Sleep实现
  6. 唤醒机制(Wake)
  7. 主函数的实现
  8. Pin、取消与递归
  9. 总结

引言

use futures::future;
use std::time::Duration;

async fn foo(n: u64) {
println!("start {n}");
tokio::time::sleep(Duration::from_secs(1)).await;
println!("end {n}");
}

#[tokio::main]
async fn main() {
let mut futures = Vec::new();
for n in 1..=10 {
futures.push(foo(n));
}
let joined_future = future::join_all(futures);
joined_future.await;
}

在引言部分,我们展示了一个异步Rust的示例代码,但并未解释其内部工作原理。

这留下了几个疑问:什么是异步函数及其返回的“futures”? join_all函数的作用是什么? tokio::time::sleepstd::thread::sleep有何不同?

为了回答这些问题,我们将把这些异步组件转换为普通的、非异步的Rust代码。

我们会发现,复制foojoin_all并不困难,但编写自定义的sleep函数则更为复杂。

让我们开始吧。

Futures概述

什么是Futures

在Rust的异步编程中,Future是一个核心概念

一个Future代表了一个可能尚未完成的计算,类似于一个占位符,未来某个时刻会产生一个结果

通过asyncawait语法,Rust允许我们以同步的方式编写异步代码,极大地简化了异步编程的复杂性

Futures的基本组成

一个Future主要由以下几个部分组成

  • Pin:一种指针包装器,用于确保内存中某个位置的数据不会被移动。对于某些需要**自引用的Future**来说,Pin是必需的。
  • Context:上下文信息,包含一个Waker
  • Waker:用于在Future需要被重新调度时唤醒它
  • PollFuturepoll方法返回一个Poll枚举,指示Future是已完成(Ready)还是尚未完成(Pending)。

下面我们通过具体的代码示例来深入理解这些概念。

代码示例解析

foo函数的异步实现

首先,我们来看一个异步函数foo的示例:

async fn foo(n: u64) {
println!("start {n}");
tokio::time::sleep(Duration::from_secs(1)).await;
println!("end {n}");
}

这个函数做了以下几件事:

  1. 打印开始信息。
  2. 异步等待1秒钟。
  3. 打印结束信息。

通过async关键字,这个函数返回一个Future而不是立即执行

调用者可以选择等待这个`Future`完成

foo函数的同步实现

为了更好地理解异步函数的工作原理,我们将foo函数转换为一个同步的、非异步的版本:

use std::pin::Pin;
use std::future::Future;
use std::task::{Context, Poll};
use std::time::Duration;

fn foo(n: u64) -> Foo {
let started = false;
let duration = Duration::from_secs(1);
let sleep = Box::pin(tokio::time::sleep(duration));
Foo { n, started, sleep }
}

struct Foo {
n: u64,
started: bool,
sleep: Pin<Box<tokio::time::Sleep>>,
}

impl Future for Foo {
type Output = ();

fn poll(mut self: Pin<&mut Self>, context: &mut Context) -> Poll<()> {
if !self.started {
println!("start {}", self.n);
self.started = true;
}
if self.sleep.as_mut().poll(context).is_pending() {
return Poll::Pending;
}
println!("end {}", self.n);
Poll::Ready(())
}
}

解析:

  1. 函数定义:
  • foo函数现在返回一个Foo结构体,而不是一个Future
  • Foo结构体包含:
    • 一个计数器n
    • 一个标志started,用于跟踪是否已经开始执行。
    • 一个被Pin包装的sleep future。
  1. Future实现:
  • Foo实现了Future trait。
  • poll方法中:
    • 如果尚未开始,打印开始信息并设置startedtrue
    • 调用sleeppoll方法。
      • 如果sleep还未完成,返回Poll::Pending
      • 如果sleep完成,打印结束信息并返回Poll::Ready(())

通过这种方式,我们手动实现了一个简单的Future,它模拟了异步函数的行为。

JoinAll的实现

接下来,我们来看join_all函数的实现。join_all用于等待一组Future全部完成。

异步实现

在异步代码中,使用join_all如下:

async fn main() {
let futures = vec![foo(1), foo(2), foo(3)];
futures::future::join_all(futures).await;
}

同步实现

我们将join_all转换为同步的、非异步的版本:

fn join_all<F: Future>(futures: Vec<F>) -> JoinAll<F> {
JoinAll {
futures: futures.into_iter().map(Box::pin).collect(),
}
}

struct JoinAll<F> {
futures: Vec<Pin<Box<F>>>,
}

impl<F: Future> Future for JoinAll<F> {
type Output = ();

fn poll(mut self: Pin<&mut Self>, context: &mut Context) -> Poll<()> {
let is_pending = |future: &mut Pin<Box<F>>| {
future.as_mut().poll(context).is_pending()
};
self.futures.retain_mut(is_pending);
if self.futures.is_empty() {
Poll::Ready(())
} else {
Poll::Pending
}
}
}

解析:

  1. 函数定义:
    • join_all函数接收一个Future的向量,并返回一个JoinAll结构体。
  2. 结构体定义:
    • JoinAll结构体包含一个FutureVec,每个FutureBox::pin包装,以确保它们在内存中的位置固定。
  3. Future实现:
    • poll方法中:
      • 使用retain_mut方法保留所有尚未完成的Future
      • 如果所有Future都完成了,返回Poll::Ready(())
      • 否则,返回Poll::Pending

通过这种方式,我们手动实现了一个能够等待多个Future完成的Future

自定义Sleep实现

现在,让我们尝试实现自己的sleep函数。我们希望它能够异步地等待指定的时间。

异步实现

在异步代码中,使用sleep如下:

async fn foo(n: u64) {
println!("start {n}");
tokio::time::sleep(Duration::from_secs(1)).await;
println!("end {n}");
}

同步实现

我们将sleep函数转换为同步的、非异步的版本:

fn sleep(duration: Duration) -> Sleep {
let wake_time = Instant::now() + duration;
Sleep { wake_time }
}

struct Sleep {
wake_time: Instant,
}

impl Future for Sleep {
type Output = ();

fn poll(self: Pin<&mut Self>, _: &mut Context) -> Poll<()> {
if Instant::now() >= self.wake_time {
Poll::Ready(())
} else {
Poll::Pending
}
}
}

问题:

尽管代码逻辑看起来正确,运行时发现sleep函数无法正确唤醒,导致程序挂起。

这是因为Future::poll方法在返回Poll::Pending时需要安排一次唤醒,而当前的实现并未完成这一点。

唤醒机制(Wake)

为了让sleep函数能够在指定时间后正确唤醒,我们需要实现唤醒机制。这涉及到ContextWaker的使用。

理解ContextWaker

  • Context:包含一个Waker,用于在Future需要被重新调度时唤醒它。
  • Waker:用于通知执行器,Future已经准备好被再次poll

修改Sleep实现

我们需要在sleep函数的poll方法中安排唤醒:

use std::sync::{Mutex, Arc};
use std::collections::BTreeMap;
use std::task::Waker;

static WAKE_TIMES: Mutex<BTreeMap<Instant, Vec<Waker>>> =
Mutex::new(BTreeMap::new());

impl Future for Sleep {
type Output = ();

fn poll(self: Pin<&mut Self>, context: &mut Context) -> Poll<()> {
if Instant::now() >= self.wake_time {
Poll::Ready(())
} else {
let mut wake_times = WAKE_TIMES.lock().unwrap();
let wakers_vec = wake_times.entry(self.wake_time).or_default();
wakers_vec.push(context.waker().clone());
Poll::Pending
}
}
}

解析:

  1. 全局唤醒时间表:
    • 使用BTreeMap按时间排序存储唤醒时间和对应的Waker
  2. poll中注册Waker
    • 如果当前时间未到唤醒时间,将Waker添加到WAKE_TIMES中对应的时间点。
  3. 主循环的实现:
    • 主函数会监视WAKE_TIMES,并在到达唤醒时间时调用相应的Waker,从而重新poll相关的Future

主函数的实现

接下来,我们实现一个主函数,负责调度和执行所有的Future

fn main() {
let mut futures = Vec::new();
for n in 1..=10 {
futures.push(foo(n));
}
let mut joined_future = Box::pin(join_all(futures));
let waker = futures::task::noop_waker();
let mut context = Context::from_waker(&waker);
while joined_future.as_mut().poll(&mut context).is_pending() {
// 获取下一个唤醒时间
let mut wake_times = WAKE_TIMES.lock().unwrap();
let next_wake = wake_times.keys().next().expect("sleep forever?");
thread::sleep(next_wake.saturating_duration_since(Instant::now()));
// 唤醒所有到期的Waker
while let Some(entry) = wake_times.first_entry() {
if *entry.key() <= Instant::now() {
entry.remove().into_iter().for_each(Waker::wake);
} else {
break;
}
}
}
}

解析:

  1. 初始化Future
    • 创建多个foo函数的Future实例,并将它们传递给join_all函数,得到一个聚合的Future
  2. 创建Context
  • 使用noop_waker创建一个空的Waker,并构建Context
  1. 轮询Future
  • 在循环中不断poll聚合的Future
  • 获取下一个唤醒时间,并让主线程休眠到该时间。
  • 唤醒所有到期的Waker,以重新调度相应的Future

结果:

这样,我们实现了一个简单的异步运行时,能够正确地调度和执行多个Future,并解决了之前的“忙循环”问题。

PinCancellationRecursion

Pin

Pin是Rust中的一个关键类型,用于确保某个数据在内存中的位置固定,防止其被移动

对于**自引用的Future**来说,Pin是必需的。

示例:

struct Foo {
n: u64,
n_ref: &u64,
started: bool,
sleep: Pin<Box<tokio::time::Sleep>>,
}

在这种情况下,Pin确保Foo结构体在内存中的位置固定,避免n_ref被移动。

取消(Cancellation)

  • 异步函数具有取消的能力。当我们不再需要某个Future时,可以通过不再poll它来取消它。
  • tokio提供了tokio::time::timeout等工具来实现超时取消

示例:

struct Timeout<F> {
sleep: Pin<Box<tokio::time::Sleep>>,
inner: Pin<Box<F>>,
}

impl<F: Future> Future for Timeout<F> {
type Output = Option<F::Output>;

fn poll(
mut self: Pin<&mut Self>,
context: &mut Context,
) -> Poll<Self::Output> {
if let Poll::Ready(output) = self.inner.as_mut().poll(context) {
return Poll::Ready(Some(output));
}
if self.sleep.as_mut().poll(context).is_ready() {
return Poll::Ready(None);
}
Poll::Pending
}
}

fn timeout<F: Future>(duration: Duration, inner: F) -> Timeout<F> {
Timeout {
sleep: Box::pin(tokio::time::sleep(duration)),
inner: Box::pin(inner),
}
}

递归(Recursion)

  • 异步函数不支持直接递归调用,因为这会导致无限大小的Future
  • 解决方法是通过Box::pin进行堆分配:
async fn factorial(n: u64) -> u64 {
if n == 0 {
1
} else {
let recurse = Box::pin(factorial(n - 1));
n * recurse.await
}
}

这样可以避免无限大小的问题,但需要堆分配,可能带来性能开销

Rust中Future的生命周期流程分析

以下是对Rust中Future生命周期的流程中每个阶段的详细分析(如顶部图片所示):

  1. Future 创建阶段

步骤:

  • 创建异步函数
  • 编译器将异步函数转换为状态机
  • 生成 Future 实例

Rust中的异步函数(async fn)在编译时会被转换为一个状态机。 这种转换使得异步函数能够在执行过程中保存其状态,以便在未来的某个时间点继续执行。 编译器生成的状态机实现了Future trait,从而生成一个Future实例。

  1. 执行器处理阶段

步骤:

  • Future 提交到执行器
  • 分配执行上下文
  • 创建 Waker 对象

Future实例一旦创建,就需要被执行器(如tokio或async-std)管理。 执行器负责调度和驱动Future的执行。 执行器会为每个Future分配一个执行上下文(Context),并创建一个Waker对象,用于在Future需要被重新调度时唤醒它。

  1. Future 状态轮询阶段

步骤:

  • 调用 poll 方法
  • 检查完成状态
  • 未完成则注册 Waker
  • 返回 Pending 状态

执行器通过调用Futurepoll方法来推进其执行。poll方法会检查Future是否已经完成:

  • 如果Future已经完成,返回Poll::Ready
  • 如果Future尚未完成,返回Poll::Pending,并注册Waker,以便在未来某个时间点唤醒执行器重新调度该Future
  1. 唤醒机制阶段

步骤:

  • 等待外部事件
  • 事件就绪时触发
  • 调用 wake() 方法
  • Future 重新入队等待执行

Future处于Poll::Pending状态时,它通常会等待某个外部事件(如I/O操作完成或定时器到期)。

一旦事件就绪,相关的Waker会被调用,通知执行器重新调度该Future进行下一步的poll操作。

  1. 完成阶段

步骤:

  • 返回 Ready 状态
  • 获取执行结果
  • 清理相关资源

Future完成其任务后,poll方法会返回Poll::Ready, 执行器随后可以获取Future的执行结果,并进行必要的资源清理工作。

以下是对整个流程的总结:

  1. 创建阶段
    • 定义异步函数,编译器将其转换为状态机,生成Future实例。
  2. 执行器处理阶段
    • Future提交给执行器,分配执行上下文,创建Waker对象。
  3. 状态轮询阶段
    • 执行器调用poll方法,检查Future是否完成,未完成则注册Waker并返回Pending
  4. 唤醒机制阶段
    • 外部事件就绪时,Waker被调用,Future重新入队等待执行。
  5. 完成阶段
    • Future返回Ready状态,执行器获取结果并清理资源。

但为了更全面地理解Future的生命周期,以下几点也值得注意:

  • 执行器的具体实现:
    • 不同的执行器(如tokioasync-std)在具体的调度和任务管理上可能有所不同,但总体流程相似。
  • 多任务调度:
    • 执行器通常会同时管理多个Future,通过异步事件驱动机制高效地调度它们。
  • 错误处理:
    • 在实际应用中,Future可能会因为各种原因失败,执行器需要能够处理这些错误。
  • 资源管理:
    • Future的生命周期结束后,相关的资源(如内存、文件句柄)需要被正确释放,以防止资源泄漏。

通过理解和掌握这些流程和细节,开发者可以更高效地编写和优化Rust中的异步代码,充分利用Rust在并发和异步编程中的强大优势。

总结

在Rust中,Future是异步编程的核心。

通过理解Future的工作原理、如何手动实现它们,以及如何构建一个简单的异步运行时,我们可以更深入地掌握Rust的异步机制。

虽然Rust的async/await语法极大地简化了异步编程,但了解底层机制对于编写高效、可靠的异步代码至关重要。

关键要点

  • Future的基本概念:Future代表一个可能尚未完成的计算,通过poll方法驱动其完成。
  • 手动实现Future:通过实现Future trait,可以深入理解异步编程的内部机制。
  • 唤醒机制:Waker用于通知执行器,Future已经准备好被重新poll
  • 构建异步运行时:了解如何手动驱动Future的执行,有助于理解像tokio这样的异步运行时的工作原理。
  • 额外概念:PinCancellationRecursion等高级概念,进一步增强对异步编程的掌握。

通过本文的学习,相信您对Rust的异步编程有了更深入的理解。

在接下来的篇章中,我们将继续探讨异步任务(Tasks)和异步IO(IO)的相关内容,进一步完善您的异步编程知识体系。

参考资料

鱼雪

Rust作为一门注重内存安全和并发性能的现代编程语言,广泛应用于系统编程、网络服务、嵌入式开发等领域。

在多线程环境中,如何安全高效地共享数据结构是开发者常面临的挑战之一。

Arc<Mutex<HashMap<K, V>>> 是一种常见的并发数据结构组合,但它并非在所有场景下都是最佳选择。

本文将深入探讨什么是 Arc<Mutex<HashMap<K, V>>>,为什么会使用它,使用过程中存在的问题, 以及在什么情况下适合或不适合使用它,并介绍一些更优的替代方案。

目录

  1. 什么是 Arc<Mutex<HashMap<K, V>>>
  2. 为什么会使用 Arc<Mutex<HashMap<K, V>>>
  3. 使用 Arc<Mutex<HashMap<K, V>>> 存在的问题
    • 粗粒度锁导致的争用
    • 死锁风险
    • 锁污染
    • Mutex 锁定与解锁的开销
    • 缺乏细粒度控制
  4. 什么时候使用或不使用 Arc<Mutex<HashMap<K, V>>>
  5. 替代解决方案
    • DashMap
    • RwLock<HashMap<K, V>>
    • tokio::sync::Mutex
  6. 总结
  7. 参考

什么是 Arc<Mutex<HashMap<K, V>>>

在Rust中,多线程环境下共享数据通常需要通过智能指针和同步原语来实现

Arc<Mutex<HashMap<K, V>>> 是一种常见的组合用于在多个线程之间共享和安全地访问一个 HashMap

  • Arc (std::sync::Arc):原子引用计数,用于在多个线程间共享所有权
  • Mutex (std::sync::Mutex):互斥锁,确保在任意时刻只有一个线程可以访问被保护的数据
  • HashMap<K, V>键值对存储的数据结构

组合起来,Arc<Mutex<HashMap<K, V>>> 允许多个线程通过 Arc 共享对 HashMap 的所有权, 并通过 Mutex 确保对 HashMap 的访问是线程安全的。

示例代码

use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use std::thread;

fn main() {
// 创建一个被Arc<Mutex>包装的共享HashMap
let map = Arc::new(Mutex::new(HashMap::new()));

// 创建多个线程,每个线程向HashMap插入一个键值对
let handles: Vec<_> = (0..5).map(|i| {
let map = Arc::clone(&map);
thread::spawn(move || {
let mut guard = map.lock().unwrap();
guard.insert(i, i * 10);
println!("Thread {} inserted {} -> {}", i, i, i * 10);
})
}).collect();

// 等待所有线程完成
for handle in handles {
handle.join().unwrap();
}

// 打印HashMap的最终状态
let final_map = map.lock().unwrap();
println!("Final map: {:?}", *final_map);
}

输出示例:

Thread 0 inserted 0 -> 0
Thread 1 inserted 1 -> 10
Thread 2 inserted 2 -> 20
Thread 3 inserted 3 -> 30
Thread 4 inserted 4 -> 40
Final map: {0: 0, 1: 10, 2: 20, 3: 30, 4: 40}

为什么会使用 Arc<Mutex<HashMap<K, V>>>

使用 Arc<Mutex<HashMap<K, V>>> 主要出于以下几个原因:

  1. 共享所有权Arc 允许多个线程拥有对同一个 HashMap 的所有权,确保数据在多线程环境下的共享。
  2. 线程安全Mutex 提供了互斥锁,确保同一时间只有一个线程可以访问或修改 HashMap,防止数据竞争和不一致性。
  3. 简单易用:这种组合方式在Rust中非常直观,适用于简单的并发场景,开发者容易理解和实现。

然而,随着应用规模的扩大和并发需求的增加,Arc<Mutex<HashMap<K, V>>> 的局限性也逐渐显现。

使用 Arc<Mutex<HashMap<K, V>>> 存在的问题

尽管 Arc<Mutex<HashMap<K, V>>> 在简单的多线程场景下效果良好,但在高并发和复杂应用中,可能会带来以下问题:

粗粒度锁导致的争用

问题描述:

  • 当整个 HashMap 被一个 Mutex 锁定时,任何对 HashMap 的访问或修改操作都需要先获得锁。 这种锁定方式被称为粗粒度锁定。粗粒度锁定会导致多个线程在访问不同键时相互阻塞,降低并发性能。

示例代码:

use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use std::thread;

fn main() {
let map = Arc::new(Mutex::new(HashMap::new()));

let handles: Vec<_> = (0..5).map(|i| {
let map = Arc::clone(&map);
thread::spawn(move || {
let mut guard = map.lock().unwrap();
guard.insert(i, i * 10);
println!("Thread {} inserted {} -> {}", i, i, i * 10);
})
}).collect();

for handle in handles {
handle.join().unwrap();
}

let final_map = map.lock().unwrap();
println!("Final map: {:?}", *final_map);
}

问题展示:

  • 即使多个线程访问不同的键,它们仍然需要等待锁释放,导致并发性能下降。

解决方案:

  • 使用细粒度锁或无锁数据结构,如 DashMap,可以显著提高并发性能。

死锁风险

问题描述:

  • Mutex 可能导致死锁,尤其是在多个线程尝试以不同顺序获取多个锁时。 虽然Rust的 Mutex 在恐慌或析构时会释放锁,但程序逻辑中的锁获取顺序不一致仍可能引发死锁。

示例代码:死锁

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

fn main() {
let resource_a = Arc::new(Mutex::new(0));
let resource_b = Arc::new(Mutex::new(0));

let r1 = Arc::clone(&resource_a);
let r2 = Arc::clone(&resource_b);
let handle1 = thread::spawn(move || {
let _lock_a = r1.lock().unwrap();
println!("Thread 1: Locked resource A");
thread::sleep(Duration::from_millis(50));
let _lock_b = r2.lock().unwrap();
println!("Thread 1: Locked resource B");
});

let r1 = Arc::clone(&resource_a);
let r2 = Arc::clone(&resource_b);
let handle2 = thread::spawn(move || {
let _lock_b = r2.lock().unwrap();
println!("Thread 2: Locked resource B");
thread::sleep(Duration::from_millis(50));
let _lock_a = r1.lock().unwrap();
println!("Thread 2: Locked resource A");
});

handle1.join().unwrap();
handle2.join().unwrap();
}

问题展示:

  • 线程1锁定 resource_a 后尝试锁定 resource_b,而线程2先锁定 resource_b 后尝试锁定 resource_a,导致两者相互等待,形成死锁。

解决方案:

  • 一致的锁定顺序:所有线程按照相同的顺序获取锁,避免循环等待。
  • 使用 try_lock:尝试获取锁,若失败则退避或重试,避免无限期等待。

锁污染

问题描述:

如果一个线程在持有锁时发生恐慌(panic),Rust的 Mutex 会将其标记为“污染”(poisoned), 后续尝试获取锁时会返回错误,增加了错误处理的复杂性。

示例代码:锁污染

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
let data = Arc::new(Mutex::new(vec![]));

let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut lock = data_clone.lock().unwrap();
lock.push(42);
println!("Thread 1: Pushed 42");
panic!("Thread 1 panicked!");
});

let _ = handle.join();

match data.lock() {
Ok(lock) => {
println!("Successfully acquired lock: {:?}", lock);
}
Err(poisoned) => {
println!("Mutex is poisoned! Recovering...");
let mut lock = poisoned.into_inner();
lock.push(99);
println!("Recovered data: {:?}", lock);
}
}
}

问题展示:

  • 线程1在持有锁时发生恐慌,导致锁被污染。主线程在尝试获取锁时需要处理错误。

解决方案:

  • 恢复数据:使用 into_inner() 方法安全地检索数据。
  • 忽略污染:如果确定数据安全,可以忽略错误。
  • 重启或中止操作:在关键系统中,可能需要重启或停止程序以防止进一步问题。

Mutex 锁定与解锁的开销

问题描述:

  • Mutex 在高并发场景下频繁的锁定与解锁操作会带来显著的性能开销,尤其是当操作需要频繁访问共享数据时。

示例代码:测量 Mutex 开销

use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::time::Instant;

const NUM_THREADS: usize = 100;
const NUM_INCREMENTS: usize = 100_000;

fn main() {
// Mutex保护的计数器
let mutex_counter = Arc::new(Mutex::new(0));
let mutex_start = Instant::now();

// 创建线程,递增Mutex保护的计数器
let mut handles = vec![];
for _ in 0..NUM_THREADS {
let counter = Arc::clone(&mutex_counter);
handles.push(thread::spawn(move || {
for _ in 0..NUM_INCREMENTS {
let mut lock = counter.lock().unwrap();
*lock += 1;
}
}));
}

// 等待所有线程完成
for handle in handles {
handle.join().unwrap();
}

let mutex_duration = mutex_start.elapsed();
println!("Mutex counter: {}", *mutex_counter.lock().unwrap());
println!("Time taken with Mutex: {:?}", mutex_duration);

// 原子计数器
let atomic_counter = Arc::new(AtomicUsize::new(0));
let atomic_start = Instant::now();

// 创建线程,递增原子计数器
let mut handles = vec![];
for _ in 0..NUM_THREADS {
let counter = Arc::clone(&atomic_counter);
handles.push(thread::spawn(move || {
for _ in 0..NUM_INCREMENTS {
counter.fetch_add(1, Ordering::SeqCst);
}
}));
}

// 等待所有线程完成
for handle in handles {
handle.join().unwrap();
}

let atomic_duration = atomic_start.elapsed();
println!("Atomic counter: {}", atomic_counter.load(Ordering::SeqCst));
println!("Time taken with AtomicUsize: {:?}", atomic_duration);
}

问题展示:

  • 在高并发环境下,使用 Mutex 保护的计数器耗时明显多于使用原子操作的计数器。

输出示例:

Mutex counter: 10000000
Time taken with Mutex: 2.345678123s
Atomic counter: 10000000
Time taken with AtomicUsize: 0.123456789s

解决方案:

  • 在仅需执行简单操作(如递增计数器)时,使用原子操作(AtomicUsize)可以避免锁的开销,提升性能。

缺乏细粒度控制

问题描述:

  • Mutex<HashMap<K, V>> 锁定整个 HashMap,无法对单个键值对进行独立控制。 即便操作的是不同的键,仍需序列化,限制了并发性。

示例代码:缺乏细粒度控制

use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use std::thread;
use std::time::Duration;

fn main() {
let map = Arc::new(Mutex::new(HashMap::new()));

// 插入一些初始值
{
let mut guard = map.lock().unwrap();
guard.insert("key1", 10);
guard.insert("key2", 20);
}

// 线程1:读取"key1"
let map_reader = Arc::clone(&map);
let reader_handle = thread::spawn(move || {
let lock = map_reader.lock().unwrap();
let value = lock.get("key1").copied().unwrap_or(0);
println!("Reader thread: Read key1 -> {}", value);
});

// 线程2:更新"key2"
let map_writer = Arc::clone(&map);
let writer_handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(50)); // 确保读取线程先开始
let mut lock = map_writer.lock().unwrap();
lock.insert("key2", 30);
println!("Writer thread: Updated key2 -> 30");
});

// 等待两个线程完成
reader_handle.join().unwrap();
writer_handle.join().unwrap();

// 打印map的最终状态
let final_map = map.lock().unwrap();
println!("Final map: {:?}", *final_map);
}

问题展示:

  • 尽管读取 key1 和写入 key2 是独立操作,但由于整个 HashMap 被锁定,导致操作必须序列化,限制了并发性。

解决方案:

  • 使用细粒度锁或并发数据结构,如 DashMap,允许对不同键进行独立控制,提升并发性能。

什么时候使用或不使用 Arc<Mutex<HashMap<K, V>>>

适用场景

  • 小规模数据结构:当 HashMap 较小,锁争用不严重时,使用 Arc<Mutex<HashMap<K, V>>> 简化了代码设计。
  • 操作频率低:当对 HashMap 的操作较少或本身是串行化的,锁的开销影响较小。
  • 性能要求不高:在性能不是关键因素的应用中,Arc<Mutex> 的简洁性优于其性能缺陷。

不适用场景

  • 高并发访问:在高并发环境下,Arc<Mutex<HashMap<K, V>>> 的锁争用会显著降低性能。
  • 复杂并发操作:需要对不同键进行独立控制或进行复杂的并发操作时,Arc<Mutex<HashMap<K, V>>> 无法满足需求。
  • 性能敏感应用:在对性能有严格要求的应用中,应选择更高效的并发数据结构或同步机制。

替代解决方案

1. DashMap

介绍:

DashMap 是一个线程安全的并发哈希映射,支持细粒度锁定。它允许多个线程同时读取或写入不同的键,而不会相互阻塞。

优势:

  • 细粒度锁定:仅锁定特定键的桶,允许更高的并发性。
  • 易用性:与 HashMap 类似的API,易于上手。
  • 高性能:显著减少锁争用,提高并发性能。

示例代码:

use dashmap::DashMap;
use std::thread;

fn main() {
let map = DashMap::new();

let handles: Vec<_> = (0..5).map(|i| {
let map = map.clone();
thread::spawn(move || {
map.insert(i, i * 10);
println!("Thread {} inserted {} -> {}", i, i, i * 10);
})
}).collect();

for handle in handles {
handle.join().unwrap();
}

println!("Final map: {:?}", map);
}

输出示例:

Thread 0 inserted 0 -> 0
Thread 1 inserted 1 -> 10
Thread 2 inserted 2 -> 20
Thread 3 inserted 3 -> 30
Thread 4 inserted 4 -> 40
Final map: {0: 0, 1: 10, 2: 20, 3: 30, 4: 40}

2. RwLock<HashMap<K, V>>

介绍:

RwLock(读写锁)允许多个线程同时读取数据,但在写入时需要独占锁。适用于读多写少的场景。

优势:

  • 高并发读操作:多个读者可以并行访问数据,不会互相阻塞。
  • 灵活性:在需要写入时仍然提供独占访问。

示例代码:

use std::sync::{Arc, RwLock};
use std::collections::HashMap;
use std::thread;

fn main() {
let map = Arc::new(RwLock::new(HashMap::new()));

// 写入操作
{
let mut write_guard = map.write().unwrap();
write_guard.insert("key1", 10);
write_guard.insert("key2", 20);
}

// 读取操作
let map_reader = Arc::clone(&map);
let reader_handle = thread::spawn(move || {
let read_guard = map_reader.read().unwrap();
if let Some(value) = read_guard.get("key1") {
println!("Found: {}", value);
}
});

// 写入操作
let map_writer = Arc::clone(&map);
let writer_handle = thread::spawn(move || {
let mut write_guard = map_writer.write().unwrap();
write_guard.insert("key2", 30);
println!("Updated key2 -> 30");
});

reader_handle.join().unwrap();
writer_handle.join().unwrap();

// 打印最终状态
let final_map = map.read().unwrap();
println!("Final map: {:?}", *final_map);
}

输出示例:

Found: 10
Updated key2 -> 30
Final map: {"key1": 10, "key2": 30}

3. tokio::sync::Mutex(适用于异步代码)

介绍:

在异步应用中,应使用 tokio::sync::Mutex 而不是标准库的 std::sync::Mutex

它允许线程在等待锁时让出,避免阻塞整个线程,适合异步运行时高效管理任务。

优势:

  • 异步兼容:不会阻塞异步任务,允许其他任务在等待锁时运行。
  • 提高异步运行时效率:任务可以在等待锁时让出,提升整体并发性能。

示例代码:

use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::task;
use std::time::Duration;

#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0));

let mut handles = vec![];

for _ in 0..5 {
let counter = Arc::clone(&counter);
let handle = task::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
let mut lock = counter.lock().await;
*lock += 1;
println!("Counter incremented to: {}", *lock);
});
handles.push(handle);
}

for handle in handles {
handle.await.unwrap();
}

let final_value = *counter.lock().await;
println!("Final counter value: {}", final_value);
}

输出示例:

Counter incremented to: 1
Counter incremented to: 2
Counter incremented to: 3
Counter incremented to: 4
Counter incremented to: 5
Final counter value: 5

为何使用 tokio::sync::Mutex:

  • 在异步应用中,使用 std::sync::Mutex 会阻塞整个线程,阻碍其他异步任务的运行
  • tokio::sync::Mutex 允许任务在等待锁时让出,确保异步运行时的高效调度和执行

总结

在Rust中,Arc<Mutex<HashMap<K, V>>> 是一种常见的并发数据结构组合,适用于简单和低并发的场景。

然而,在高并发和复杂应用中,它的锁争用、死锁风险、锁污染以及性能开销等问题使其不再是最佳选择。

幸运的是,Rust生态系统提供了多种替代方案,如 DashMap、RwLocktokio::sync::Mutex, 这些工具能够更高效地处理并发访问,提升应用性能和可靠性。

选择合适的并发数据结构和同步机制,是编写高效、安全Rust程序的关键。根据具体应用场景,权衡性能与复杂性, 做出最适合的设计选择,才能充分发挥Rust语言在并发编程中的优势。

希望这篇博客能够帮助您更好地理解在Rust中使用 Arc<Mutex<HashMap<K, V>>> 的潜在问题及其替代方案, 从而在实际项目中做出更明智的选择。

参考

鱼雪

ISRG 近年来一直在大力投资 Rustls TLS 库。我们的目标是创建一个既能保证内存安全又在性能上领先的库。

今年一月,我们发布了一篇关于我们性能之旅起点的文章。 从那时起,我们取得了长足的进步,今天我们很高兴分享 Rustls 性能的最新进展。

什么是 Rustls?

Rustls 是一个内存安全的 TLS 实现,专注于性能。它已经可以用于生产环境,并在广泛的应用中使用。 您可以在维基百科上了解更多关于其历史的信息。

Rustls 提供 C API 和 FIPS 支持,使我们能够将内存安全和性能带给广泛的现有程序。

这一点很重要,因为 OpenSSL 及其衍生产品在互联网上被广泛使用,长期以来存在内存安全漏洞, 今年又发现了更多漏洞。

是时候让互联网摆脱基于 C 的 TLS 了。

握手性能

我们首先来看一下在相同硬件和相同资源限制下每秒可以完成的握手次数。

这些测试连接一个客户端到一个服务器,通过内存缓冲区进行,并测量客户端和服务器处理时的时间, 因此在没有网络延迟或系统调用开销的情况下,它们提供了性能的上限。

BoringSSL vs OpenSSL vs Rustls resumption performance

resumed handshakes per second

Rustls 在每个测试场景中都领先。

吞吐量性能

接下来,我们看一下在相同硬件和相同资源限制下的吞吐量,以每秒兆字节为单位:

BoringSSL vs OpenSSL vs Rustls transfer performance

Rustls 在所有测试中也同样表现出色。

测试方法

测试是在 Debian Linux 上进行的,使用的是裸机 Intel Xeon E-2386G CPU,禁用了超线程和动态频率缩放, 并将 CPU 缩放调节器设置为所有核心的性能模式。更多细节可以在这里找到。

尝试 Rustls!

Rustls 已经可以用于生产环境,我们鼓励大家试用它。除了内存安全和出色的性能,它还提供:

  • C 和 Rust API
  • FIPS 支持
  • 后量子密钥交换(即将更新算法)
  • 加密客户端 Hello(客户端侧)
  • 操作系统信任验证器支持

链接

鱼雪

2024年10月17日,Rust 发布团队宣布发布 Rust 1.82.0。 Rust 是一门编程语言,旨在帮助每个人构建可靠且高效的软件。

如果您已经通过 rustup 安装了之前版本的 Rust,可以通过以下命令更新到 1.82.0

$ rustup update stable

如果您还没有安装 rustup,可以从我们网站的相关页面获取,并查看 1.82.0 的详细发行说明。

Rust 1.82.0 中的新特性

Cargo 信息命令

Cargo 现在有一个新的 info 子命令,用于显示注册表中某个包的信息。

这一功能满足了一个接近十年历史的请求!例如,您可以通过 cargo info cc 查看以下信息:

cc #build-dependencies
A build-time dependency for Cargo build scripts to assist in invoking the native
C compiler to compile native C code into a static archive to be linked into Rust
code.
version: 1.1.23 (latest 1.1.30)
license: MIT OR Apache-2.0
rust-version: 1.63
documentation: https://docs.rs/cc
homepage: https://github.com/rust-lang/cc-rs
repository: https://github.com/rust-lang/cc-rs
crates.io: https://crates.io/crates/cc/1.1.23
features:
jobserver = []
parallel = [dep:libc, dep:jobserver]
note: to see how you depend on cc, run `cargo tree --invert --package cc@1.1.23`

Apple 目标提升

  • macOS 在 64 位 ARM 上成为 Tier 1:Rust 目标 aarch64-apple-darwin 现在是 Tier 1 目标,表示我们对其正常工作的最高保证。
  • Mac Catalyst 目标成为 Tier 2:Mac Catalyst 是 Apple 的一项技术,允许在 Mac 上本地运行 iOS 应用程序。现在这些目标是 Tier 2,可以通过 rustup target add aarch64-apple-ios-macabi x86_64-apple-ios-macabi 下载。

精确捕获的 use<..> 语法

Rust 现在支持在某些 impl Trait 边界中使用 use<..> 语法来控制捕获哪些泛型生命周期参数。 这使得在返回位置 impl Trait 类型中捕获泛型参数更加精确。

原生语法创建原始指针

Rust 现在提供了原生语法来创建原始指针

  • addr_of!(expr) 变为 &raw const expr
  • addr_of_mut!(expr) 变为 &raw mut expr

安全项与不安全 extern

Rust 代码可以使用来自外部代码的函数和静态变量。

现在允许在 extern 块中使用 unsafe extern,并在其中标记某些项为安全使用。

不安全属性

某些 Rust 属性,如 no_mangle,可以在没有任何不安全块的情况下导致未定义行为。

现在这些属性被视为“不安全”,应该写为:

#[unsafe(no_mangle)]
pub fn my_global_function() { }

模式匹配中省略空类型

可以省略匹配空类型的模式:

use std::convert::Infallible;
pub fn unwrap_without_panic<T>(x: Result<T, Infallible>) -> T {
let Ok(x) = x;
x
}

浮点 NaN 语义和 const

Rust 现在标准化了 NaN 值的行为规则,并允许在 const fn 中使用浮点运算。

鱼雪