Skip to main content

72 posts tagged with "Rust"

View All Tags

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 中使用浮点运算。

鱼雪

概述

trait-gen 是一个提供 trait 实现生成的属性宏的库。

它允许为多种类型生成 trait 实现,而无需自定义声明宏、代码重复或通用实现,从而使代码更易于阅读和维护。

使用示例

以下是一个简单的示例:

use trait_gen::trait_gen;

#[trait_gen(T -> u8, u16, u32, u64, u128)]
impl MyLog for T {
fn my_log2(self) -> u32 {
T::BITS - 1 - self.leading_zeros()
}
}

trait_gen 属性将 T 替换为给定的类型,生成如下代码:

impl MyLog for u8 {
fn my_log2(self) -> u32 {
u8::BITS - 1 - self.leading_zeros()
}
}
impl MyLog for u16 {
fn my_log2(self) -> u32 {
u16::BITS - 1 - self.leading_zeros()
}
}
// 其他类型依此类推

使用方法

该属性放置在伪泛型实现代码之前泛型参数首先给出,后跟右箭头->)和类型参数列表

#[trait_gen(T -> Type1, Type2, Type3)]
impl Trait for T {
// ...
}

属性宏会依次将代码中的泛型参数 T 替换为后续类型(Type1、Type2、Type3),生成所有实现。

所有以 T 开头的类型路径都会被替换。

例如,T::default() 会生成 Type1::default()Type2::default() 等, 但 super::T 保持不变,因为它属于另一个作用域。

代码必须与所有类型兼容,否则编译器将触发相关错误。例如,#[trait_gen(T -> u64, f64)] 不能应用于 let x: T = 0;,因为 0 不是有效的浮点字面量。

实际类型还会替换文档注释、宏和字符串字面量中的任何 ${T} 出现。

注意事项

  • 使用字母 "T" 不是强制性的,任何类型路径都可以。例如,gen::Type 也是可以的。但为了提高可读性,建议使用简短的大写标识符。
  • 可以链式使用两个或多个属性以生成所有组合。
  • trait_gen 也可以用于类型实现。

动机

生成多个实现的方法有几种:

  1. 手动复制
  2. 使用声明宏
  3. 使用通用实现

上面的实现示例可以通过声明宏实现:

macro_rules! impl_my_log {
($($t:ty)*) => (
$(impl MyLog for $t {
fn my_log2(self) -> u32 {
$t::BITS - 1 - self.leading_zeros()
}
})*
)
}

impl_my_log! { u8 u16 u32 u64 u128 }

但这种方法冗长且比原生代码难以阅读。

我们必须每次编写自定义宏,包括其声明、模式和一些元素的转换(如参数 $t)。

此外,IDE 通常无法提供上下文帮助或在宏代码中应用重构。

使用通用实现还有其他缺点

  • 除了同一 crate 中未被通用实现覆盖的类型外,禁止任何其他实现。
  • 找到对应的 trait 并不总是可能。虽然 num crate 对原始类型提供了很多帮助,但并不是所有情况都涵盖。
  • 即使操作和常量被 trait 覆盖,也很快需要一长串 trait 约束。

示例

以下是支持的替换示例,库的集成测试中还有更多示例。

第一个示例更多是说明什么被替换,什么不被替换,而不是实际实现:

#[trait_gen(U -> u32, i32, u64, i64)]
impl AddMod for U {
fn add_mod(self, other: U, m: U) -> U {
const U: U = 0;
let zero = U::default();
let offset: super::U = super::U(0);
(self + other + U + zero + offset.0 as U) % m
}
}

扩展为(我们只展示第一个类型,u32):

impl AddMod for u32 {
fn add_mod(self, other: u32, m: u32) -> u32 {
const U: u32 = 0;
let zero = u32::default();
let offset: super::U = super::U(0);
(self + other + U + zero + offset.0 as u32) % m
}
}

复杂示例

以下示例展示了如何使用类型参数:

struct Meter<U>(U);
struct Foot<U>(U);

trait GetLength<T> {
fn length(&self) -> T;
}

#[trait_gen(U -> f32, f64)]
impl GetLength<U> for Meter<U> {
fn length(&self) -> U {
self.0 as U
}
}

该属性可以与另一个属性组合,以创建泛型组合, 实现 Meter<f32>Meter<f64>Foot<f32>Foot<f64> 的 trait:

#[trait_gen(T -> Meter, Foot)]
#[trait_gen(U -> f32, f64)]
impl GetLength<U> for T<U> {
fn length(&self) -> U {
self.0 as U
}
}

这将扩展为:

impl GetLength<f32> for Meter<f32> {
fn length(&self) -> f32 { self.0 as f32 }
}
impl GetLength<f64> for Meter<f64> {
fn length(&self) -> f64 { self.0 as f64 }
}
impl GetLength<f32> for Foot<f32> {
fn length(&self) -> f32 { self.0 as f32 }
}
impl GetLength<f64> for Foot<f64> {
fn length(&self) -> f64 { self.0 as f64 }
}

多段路径(带有 :: 的路径)和路径参数(如 <f32>)也可以用于参数中。

例如,使用 gen::U 可以避免与已经定义的单字母类型混淆。

遗留格式

早期版本中使用了较短的格式,尽管仍然支持,但可能更难阅读:

#[trait_gen(Type1, Type2, Type3)]
impl Trait for Type1 {
// ...
}

在这里,Type1 的代码将按原样生成,然后 Type2 和 Type3 将替换 Type1 以生成它们的实现。 这是等效属性的快捷方式。

替代格式

当启用 in_format 特性时,还支持替代格式:

trait-gen = { version="0.3", features=["in_format"] }

在这里,使用 in 替代箭头 ->且参数类型必须放在方括号中

#[trait_gen(T in [u8, u16, u32, u64, u128])]
impl MyLog for T {
fn my_log2(self) -> u32 {
T::BITS - 1 - self.leading_zeros()
}
}

使用此格式会发出“已弃用”的警告, 可以通过在文件顶部添加 #![allow(deprecated)] 指令或在生成的代码中添加 #[allow(deprecated)] 来关闭。

限制

trait_gen 属性的过程宏无法处理作用域,因此不支持任何与泛型参数相同字面量的类型声明。

例如,以下代码因泛型函数冲突而无法编译:

#[trait_gen(T -> u64, i64, u32, i32)]
impl AddMod for T {
type Output = T;

fn add_mod(self, rhs: Self, modulo: Self) -> Self::Output {
fn int_mod<T: Num> (a: T, m: T) -> T { // <== 错误,冲突的 'T'
a % m
}
int_mod(self + rhs, modulo)
}
}

泛型参数必须是类型路径;不能是更复杂的类型,如引用或切片。

兼容性

trait-gen crate 在 Windows 64 位和 Linux 64/32 位平台上测试了 rustc 1.58.0 及更高版本。

链接

鱼雪

在 Rust 中,我编写了很多应用程序,因此发现自己经常使用 .unwrap(),这比我编写整洁的库时要多得多。

我经常遇到的问题是,过了一天或一周后,我总是记不清当初为什么要使用 .unwrap()。我真的希望在这种情况下让应用程序崩溃吗?还是我只是匆忙证明我的其他代码有效,想要稍后再实现错误处理?

我认为有三种不同的 unwrap,每种都有不同的语义,程序员应该以不同的方式对待它们。

作为 panic!() 的 Unwrap

第一种 unwrap 是显而易见的;我之所以 unwrap,是因为如果发生这种情况,我们就应该崩溃。

一个很好的例子是在某些 Web 服务器代码中:

let app = Router::new().route("/", get(get_info));
let address_str = format!("{address}:{port}");

// 如果我们给出的地址或端口无效,我们无法做任何事情。就崩溃吧!
let addr: SocketAddr = address_str.parse().unwrap();

// 如果无法打开 tcp 套接字,我们无法做任何事情。就崩溃吧!
let listener = TcpListener::bind(&addr).await.unwrap();

// 如果我们的 Web 服务器意外崩溃,我们也应该崩溃!
axum::serve(listener, app.into_make_service()).await.unwrap();

所有这些 .unwrap() 的目的是相同的。如果我们无法做到这一点,就崩溃。

这些 unwrap 也是故意存在的。它们是为了处理真实的错误情况,而不是错误处理的占位符。此外,所有这些错误情况都是可能发生的,我们只是不想去考虑它们。

作为 unreachable!() 的 Unwrap

第二种 panic 不太明显,但尤其在编写大量静态变量时会出现。

一个很好的例子是声明正则表达式:

// 我们在这里 unwrap 不是因为不关心错误情况,而是因为
// 我们的错误情况是绝对无法到达的!
static HEXADECIMAL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("^[0-9a-f]*$").unwrap());

这个 unwrap 的目的与我们 Web 服务器示例中的不同。我们 unwrap 是因为这个错误情况根本不可能发生。尽管如此,这个 .unwrap() 是故意的,并不是错误处理的占位符。

作为 todo!() 的 Unwrap

每个在 Rust 中编写过应用程序的人都犯过使用 .unwrap() 的错误,心想“我稍后会处理错误,我只是想看看我的代码在正常路径上是否有效。”

实际上,任何在 Rust 中处理大型应用程序的人可能都花了大量时间追踪这些被遗忘的“临时”unwrap!

一个不错的例子是在快速而肮脏的 Rust 代码中:

// 啊,我稍后会更好地处理这个
let age: i32 = user_input.parse().unwrap();

// 或者...啊,这个文件存在,但我稍后会更好地处理。
let file: Vec<u8> = fs::read("data.txt").unwrap();

这段代码很肮脏,但在你只是想证明某些东西有效时,这种写法很常见。

这个 unwrap 的目的与其他的截然不同。我们 unwrap 是因为我们尚未实现错误处理。

那么,这有什么意义?

我所要指出的是,.unwrap() 在代码中可以有三种不同的原因:

  1. 如果我们无法做到这一点,我们就应该崩溃(类似 panic!())
  2. 这种情况是不可能的(类似 unreachable!())
  3. 我需要稍后处理错误(类似 todo!())

但这里绝对关键的问题是,这些信息并没有存储在代码中,而是存储在你的脑海中。

有些人会在周围写评论,比如 // TODO// cannot happen

有些人使用 .expect("todo").expect("must be valid regex")。 我认为这些都是肮脏的黑客行为,仍然无法准确保留我们为什么要 unwrap 的语义。

我们已经有了类似的“语义崩溃”的先例,使用 todo!()unreachable!() 宏。 为什么不在这里也使用它们呢?

你有什么建议?

我写了一份 RFC,提出了两个新的方法用于 ResultOption,旨在明确这些语义, 并防止对 unwrap 的混淆。

你可以在这里阅读提案,简单来说就是:

// unwrap 仍然用于类似 panic!() 的情况
TcpListener::bind(&addr).unwrap();

// 我们崩溃是因为错误处理尚未实现。
// 这种用例在原型应用程序中很常见。
let int: i32 = input.parse().todo();
let arg2 = std::env::args().nth(2).todo();
let data: Vec<u8> = fs::read("data.txt").todo();

// 这些错误状态是无法到达的。
// 这种用例在静态声明中很常见。
NonZeroU32::new(10).unreachable();
Regex::new("^[a-f]{5}$").unreachable();

它提议了 Option::todoOption::unreachableResult::todoResult::unreachable 函数。

这些函数分别与 todo!()unreachable!() 宏具有类似的目的。

如果在标准库中实现了这些功能,#[clippy::todo] 和其他特性可以指出不应进入生产环境的临时 unwrap。

如果这对你有用,或者这是你之前遇到过的问题,我很想在 RFC 中听听你的想法!对我个人来说,这些函数将带来很大的价值。

命名的附注

RFC 提到我们也可以将这些函数命名为 unwrap_todounwrap_unreachable。 我对此有些怀疑,因为 unwrap_todo 输入字符较多,我不确定在懒惰的上下文中,函数是否会被忽略。

我认为单独使用 .todo() 作为一个函数并不是特别令人困惑;它肯定没有比 .expect() 更令人困惑。

链接

鱼雪

Rust 提供了多种机制来定义全局常量和静态变量,其中 constlazy_static 是两种常见的选择。 它们各有优缺点,适用于不同的场景。

本文将详细分析 constlazy_static 的关系、优缺点及其使用场景,并提供示例代码帮助理解它们的用法。

1. constlazy_static 概述

const

  • 定义const 用于定义编译时常量。常量的值在编译时就已经确定,并且在代码中是不可变的。
  • 特性
    • 编译时初始化const 变量的值在编译时确定,内存分配也是在编译时完成的。
    • 不可变const 变量的值不可变,编译器会在编译时嵌入这些值到代码中。
    • 性能:由于在编译时初始化,const 变量不涉及运行时开销,性能较好。

lazy_static

  • 定义lazy_static 提供了在运行时初始化静态变量的功能。变量在第一次访问时被初始化,并且初始化过程是线程安全的。
  • 特性
    • 延迟初始化lazy_static 变量的初始化推迟到第一次访问时,这对于初始化代价高的变量尤其有用。
    • 线程安全lazy_static 使用同步原语(如 MutexRwLock)来确保多线程环境下的安全性。
    • 灵活性:支持在运行时进行复杂的初始化逻辑。

2. constlazy_static 的对比

2.1 性能

  • const:由于 const 变量在编译时就已确定其值,并且直接嵌入到代码中,因此不涉及运行时开销。适合那些需要高性能和确定性常量的场景。
  • lazy_static:涉及运行时初始化,因此会有初始化延迟和可能的同步开销。适用于需要复杂初始化的场景。

2.2 内存开销

  • const:常量直接嵌入到代码中,内存占用较少,开销可预测。
  • lazy_static:可能会导致较高的内存开销,尤其是存储大数据结构时。

2.3 灵活性

  • const:适用于简单、固定的值,无法处理复杂的初始化逻辑。
  • lazy_static:允许在运行时初始化变量,支持复杂的初始化逻辑和条件。

2.4 线程安全

  • const:不涉及线程安全问题,因为它们在编译时已经是不可变的。
  • lazy_static:提供线程安全的全局变量,适合多线程环境中的共享状态。

3. 示例代码与使用场景

示例 1:使用 const

// 定义一个编译时常量
const MAX_RETRIES: u32 = 5;

fn main() {
for attempt in 1..=MAX_RETRIES {
println!("Attempt {}", attempt);
}
}

使用场景

  • 常量值:适合定义那些在编译时即可确定的固定值,如数组的大小、固定的配置值等。

示例 2:使用 lazy_static

#[macro_use]
extern crate lazy_static;

use std::sync::Mutex;
use std::collections::HashMap;

lazy_static! {
static ref CONFIG: Mutex<HashMap<String, String>> = {
let mut map = HashMap::new();
map.insert("app_name".to_string(), "MyApp".to_string());
map.insert("version".to_string(), "1.0.0".to_string());
Mutex::new(map)
};
}

fn main() {
let config = CONFIG.lock().unwrap();
println!("App Name: {}", config.get("app_name").unwrap());
}

示例 3:使用 lazy_static 创建全局数据库连接池

在这个示例中,我们将展示如何使用 lazy_staticsqlx 创建一个全局的、线程安全的 PostgreSQL 数据库连接池。 代码还演示了如何在异步环境中执行查询操作。我们将使用 dotenv 来加载数据库连接信息。

代码示例

首先,在 Cargo.toml 文件中添加所需的依赖项:

[dependencies]
lazy_static = "1.4"
sqlx = { version = "0.5", features = ["postgres", "runtime-async-std"] }
tokio = { version = "1", features = ["full"] }
dotenv = "0.15"

接下来,创建一个 main.rs 文件:

use lazy_static::lazy_static;
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use std::env;
use tokio;

lazy_static! {
static ref DB_POOL: PgPool = {
// 加载环境变量
dotenv::dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

// 创建数据库连接池
PgPoolOptions::new()
.max_connections(5)
.connect_lazy(&database_url)
.expect("Failed to create pool")
};
}

#[tokio::main]
async fn main() {
// 获取数据库连接池
let pool = &*DB_POOL;

// 执行查询
let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
.fetch_one(pool)
.await
.expect("Failed to execute query");

println!("Number of users: {}", row.0);
}

代码说明

  1. 依赖项

    • lazy_static:用于定义全局静态变量。确保在多线程环境中安全地共享数据。
    • sqlx:Rust 的异步数据库库,支持多种数据库类型。这里我们使用 PostgreSQL 数据库的支持。
    • tokio:Rust 的异步运行时库,支持异步编程。
    • dotenv:从 .env 文件中加载环境变量,用于存储数据库连接信息。
  2. 环境变量

在项目根目录创建 .env 文件,并添加以下内容:

DATABASE_URL=postgres://username:password@localhost/database

其中,DATABASE_URL 是连接 PostgreSQL 数据库所需的连接字符串。请根据实际情况替换 usernamepasswordlocalhostdatabase 的值。

  1. 使用 lazy_static 创建全局数据库连接池

    • lazy_static!:定义一个全局静态变量 DB_POOL,这是一个线程安全的 PostgreSQL 连接池。
    • dotenv::dotenv().ok():加载 .env 文件中的环境变量,允许在运行时访问数据库连接 URL。
    • env::var("DATABASE_URL"):从环境变量中获取数据库连接 URL。如果未设置该环境变量,则会 panic。
    • PgPoolOptions::new().max_connections(5).connect_lazy(&database_url):使用 PgPoolOptions 创建一个连接池。max_connections(5) 设置池中最大连接数为 5,connect_lazy 方法会延迟连接,直到第一次使用时才进行实际的连接操作。
  2. 异步主函数

    • #[tokio::main]:标记 main 函数为异步,这允许使用 await 关键字。
    • &*DB_POOL:获取全局静态变量 DB_POOL 的实际值。&* 语法用于解引用 lazy_static 创建的静态变量。
    • sqlx::query_as("SELECT COUNT(*) FROM users"):执行 SQL 查询,获取 users 表中的记录数。query_as 方法将查询结果映射到一个元组 (i64,) 中。
    • .fetch_one(pool).await:异步地从数据库中获取一行结果。
    • println!("Number of users: {}", row.0):打印查询结果,即用户表中的记录数。

这个示例展示了如何使用 lazy_staticsqlx 创建一个全局的 PostgreSQL 连接池,并在异步环境中执行查询操作。

通过将连接池的创建和管理封装在 lazy_static 中,我们可以确保在多线程环境下安全地共享数据库连接池,同时利用 tokio 和异步编程模型来处理异步 I/O 操作

这种方式适合需要在整个应用程序中共享数据库连接的场景,并且需要进行复杂的初始化操作。

使用场景

  • 复杂初始化:适用于需要延迟初始化的全局状态,如配置文件缓存数据库连接等。

4. 结论

  • 使用 const:当需要在编译时确定值且这些值不会改变时,const 是一个合适的选择。 它具有较好的性能和较低的内存开销,但只能处理简单的、编译时已知的值。
  • 使用 lazy_static当需要在运行时进行初始化或需要复杂的初始化逻辑时lazy_static一个有效的解决方案。 它提供了线程安全的全局变量,但会引入一定的运行时开销。

在实际开发中,根据具体的需求选择合适的机制可以帮助优化性能、简化代码,并确保程序的正确性和安全性。

鱼雪

这是#[wasm_bindgen]的"Hello, world!"示例,展示如何设置项目, 将函数导出到JS,从JS调用并在Rust中调用警报功能。

1. 创建项目

cargo new hello-world --lib
cd hello-world

2. 添加依赖

cargo add wasm-bindgen

3. Cargo.toml

Cargo.toml 列出了 wasm-bindgen crate 作为一个依赖项。 值得注意的是,crate-type = ["cdylib"],这在当今主要用于 wasm 最终工件。

[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.92"
note

[lib] 部分指定了库的类型为 cdylib,这意味着它将被编译为共享库

适合与其他语言(如 JavaScript)进行交互,特别是在 WebAssembly 的上下文中。

在 Rust 的 Cargo.toml 文件中,crate-type 可以指定以下几种类型:

  • lib: 默认类型,生成一个库文件(.rlib),用于其他 Rust 代码的依赖。
  • cdylib: 生成一个动态库,适合与其他语言(如 C、JavaScript)进行交互,通常用于 WebAssembly 项目。
  • rlib: 生成一个 Rust 静态库,供其他 Rust crate 使用。
  • dylib: 生成一个动态库,主要用于 Rust 生态内部的共享。
  • staticlib: 生成一个静态库,通常用于与 C/C++ 代码的交互。

你可以根据项目的需求选择合适的 crate-type

4. 编写src/lib.rs

src/lib.rs 文件包含了一个简单的函数,该函数使用 #[wasm_bindgen] 属性导出到 JavaScript。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}

上述代码中,greet 函数使用 #[wasm_bindgen] 属性导出到 JavaScript。

alert 函数是一个外部函数,它在 JavaScript 中实现。使用 extern "C" 来声明它, 然后可以在Rust代码中调用从JavaScript导入的函数。

5. 编译为WebAssembly

wasm-pack build --target web

上述命令将生成一个 pkg 目录,其中包含了编译好的 WebAssembly 模块和 TypeScript(JavaScript) 包装器。 使用 wasm-pack build --target web 来生成一个可以在浏览器中运行的包, 是一个完整的 npm 包,直接按照包的方式直接导入到 JavaScript 项目中。

如果是wasm-pack build,则可以构建一个可以在 Node.js 中运行的包。

wasm-pack build生成pkg目录

6. 在JavaScript中调用

index.js
// /path/to/hello-world/index.js

import { greet } from './pkg';

greet('World');

7. 创建npm包

pnpm init
# 或
npm init

会在当前目录下生成一个package.json文件,此文件是npm包的配置文件

8. 添加依赖

pnpm add webpack webpack-cli webpack-dev-server html-webpack-plugin -D
# 或
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D

上述几个包的作用是:

  1. webpack Webpack 是一个模块打包工具,主要用于将多个模块(如 JavaScript、CSS、图片等)打包成一个或多个文件。它可以处理依赖关系,优化资源,并支持各种加载器和插件,以满足不同的构建需求。
  2. webpack-cli webpack-cli 是 Webpack 的命令行接口,允许用户通过命令行执行 Webpack 的构建任务。它提供了一些命令和选项,使得用户可以更方便地配置和运行 Webpack,而不需要直接在代码中进行设置。
  3. webpack-dev-server webpack-dev-server 是一个用于开发的服务器,提供了热模块替换(HMR)功能,可以在代码更改时自动刷新浏览器。它使得开发过程更加高效,能够快速查看修改后的效果,而无需手动刷新页面。
  4. html-webpack-plugin html-webpack-plugin 是一个 Webpack 插件,用于生成 HTML 文件。它可以自动将打包后的 JavaScript 和 CSS 文件插入到生成的 HTML 文件中,从而简化了手动管理 HTML 的过程。它还支持模板引擎,可以根据需要自定义生成的 HTML。

9. 创建webpack.config.js

webpack.config.js
// /path/to/hello-world/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
plugins: [
new HtmlWebpackPlugin(),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, ".")
}),
],
mode: 'development',
experiments: {
asyncWebAssembly: true
}
};

上述代码的作用是:

  • 引入 Node.js 内置的 path 模块,方便处理文件路径。
  • 引入 HtmlWebpackPlugin,用于生成 HTML 文件。
  • 引入 webpack,虽然在这个配置中没有直接使用,但通常用于访问 Webpack 的功能。
  • 引入 WasmPackPlugin,用于处理 Rust 编写的 WebAssembly 模块
  • entry: 指定应用程序的入口文件,这里是 index.js。即这里是我们在第6步中创建的文件。
  • output: 指定输出文件的配置:
  • path: 输出目录,使用 path.resolve 来确保路径正确,输出到 dist 文件夹。
  • filename: 输出的文件名,这里是 index.js
  • plugins: 配置使用的插件:
  • HtmlWebpackPlugin: 自动生成 HTML 文件,包含打包后的 JavaScript 代码。
  • WasmPackPlugin: 配置 WasmPack 插件,crateDirectory 指定 Rust crate 的路径,这里是当前目录。
  • mode: 设置为 development,表示当前环境是开发模式,启用一些开发时的功能(如更好的错误信息)。
  • experiments: 启用 Webpack 的实验功能,这里开启了 asyncWebAssembly,允许使用异步加载 WebAssembly 模块。

10. 配置webpack服务

{
"name": "hello-world",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"serve": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.7.0",
"html-webpack-plugin": "^5.6.0",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
}
}

配置package.json文件,添加buildserve脚本,用于构建和启动服务。

11. 编译

pnpm run build
# 或
npm run build

使用pnpm构建代码

12. 运行

pnpm run server
# 或
npm run server

使用pnpm启动服务

打开浏览器,访问 http://localhost:8080,在控制台中可以看到输出 Hello, World!。 webpack默认的服务端口是8080,如果端口被占用,可以在webpack.config.js中配置。

浏览器中显示Hello, World!

整个项目代码结构

整个项目代码结构

链接

可能会遇到的问题

  1. wasm-pack build 时,可能会遇到以下错误:
wasm-pack build
[INFO]: 🎯 Checking for the Wasm target...
[INFO]: 🌀 Compiling to Wasm...
Compiling syn v2.0.72
Compiling wasm-bindgen-backend v0.2.92
Compiling wasm-bindgen-macro-support v0.2.92
Compiling wasm-bindgen-macro v0.2.92
Compiling wasm-bindgen v0.2.92
Compiling hello-world v0.1.0 (/Users/hal/tutorials/wasm-examples/hello-world)
Finished `release` profile [optimized] target(s) in 54.81s
[INFO]: ⬇️ Installing wasm-bindgen...
[INFO]: found wasm-opt at "/Users/hal/bin/binaryen/bin/wasm-opt"
[INFO]: Optimizing wasm binaries with `wasm-opt`...
dyld[46869]: Symbol not found: __ZTVN4wasm10PassRunnerE
Referenced from: <7458E7FE-3DC2-3048-B182-5DF54E45F6D4> /Users/hal/bin/binaryen/bin/wasm-opt
Expected in: <F032F917-10CC-3569-8C1E-2299E90E789F> /usr/local/lib/libbinaryen.dylib
Error: failed to execute `wasm-opt`: exited with signal: 6 (SIGABRT)
full command: "/Users/hal/bin/binaryen/bin/wasm-opt" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm" "-o" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm-opt.wasm" "-O"
To disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`.
Caused by: failed to execute `wasm-opt`: exited with signal: 6 (SIGABRT)
full command: "/Users/hal/bin/binaryen/bin/wasm-opt" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm" "-o" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm-opt.wasm" "-O"
To disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`
  • 解决方法1:

Cargo.toml 中添加 wasm-opt = false。 禁用 wasm-opt

[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.92"

[package.metadata.wasm-pack]
wasm-opt = false
  • 解决方法2:

安装 binaryen,并且将 binaryen 库的路径添加到动态库的环境变量中,DYLD_LIBRARY_PATH

brew install binaryen

在Mac系统上是这样,Linux系统上可能是 LD_LIBRARY_PATH。而且这里是使用Homebrew安装的,如果是其他方式安装的,可能路径不一样。

export DYLD_LIBRARY_PATH="/opt/homebrew/lib:$DYLD_LIBRARY_PATH"
鱼雪