Rust 并发轻松上手:Arc 与 Mutex 指南

更新于 2026-01-15

Amza 2025-03-10

在 Rust 中,并发是一种提升性能的强大工具,但也伴随着挑战——主要集中在数据安全和同步问题上。标准库中的 Arc(原子引用计数)和 Mutex(互斥锁)是两个关键工具,用于在线程之间管理共享数据。但它们是如何协同工作的?我们又该如何在高级场景中高效地使用它们?

在本篇博客中,我们将通过一个结合了 ArcMutex 的高级示例,逐步构建解决方案,以管理多个线程之间的共享可变状态。我们会一步步讲解关键概念。

读完本文后,你将对如何在 Rust 中安全、高效地使用 ArcMutex 进行并发编程有扎实的理解。

让我们从一个非常基础的程序开始,介绍 ArcMutex


第一步:搭建基础程序

我们的目标是创建一个共享的、可变的计数器,允许多个线程并发地对其进行更新。首先,我们将从一个最小化的设置开始,启动若干线程。

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

fn main() {
    // 创建一个受 Mutex 保护的计数器,并用 Arc 包裹以实现共享所有权
    let counter = Arc::new(Mutex::new(0));

    // 创建多个线程来操作该计数器
    let mut handles = vec![];

    for _ in 0..5 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // 锁定 Mutex 以访问并修改计数器
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
            println!("Counter: {}", num);
        });

        handles.push(handle);
    }

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

解释:

  • Arc:这是一个线程安全的引用计数指针。它允许多个线程安全地共享同一对象的所有权。每个线程都会增加引用计数,当所有线程都完成后,对象会被自动释放。
  • MutexMutex 类型确保在任意时刻只有一个线程可以访问其包装的数据。当一个线程想要修改数据时,必须先锁定 Mutex,并在完成后自动解锁。

此处发生了什么?

  • main 函数创建了一个 Mutex 来保存一个整数(初始值为 0)。
  • Arc::new(Mutex::new(0)) 确保该计数器可以被安全地在线程之间共享。
  • 我们生成了 5 个线程,每个线程都会锁定 Mutex,修改计数器,并打印当前值。
  • 所有线程都在操作同一个共享计数器,但由于它们在修改前都锁定了 Mutex,因此任意时刻只有一个线程能访问它。

核心概念详解

Arc:安全的共享指针

Rust 安全并发的基石之一是所有权和借用机制。然而,在处理线程时,所有权会变得棘手,因为多个线程需要访问相同的数据。这时就轮到 Arc 登场了。

  • 引用计数Arc 确保只要至少有一个线程持有对该数据的引用,数据就不会被释放。
  • 原子性Arc 内部使用原子操作来管理引用计数,这使其可以安全地跨线程使用。
  • 克隆Arc::clone(&counter) 并不会克隆 Arc 内部的数据,而是增加引用计数,从而允许另一个线程拥有它。

Mutex:对数据的独占访问

Rust 的 Mutex 确保在任意给定时间点,只有一个线程可以访问其包装的数据。这可以防止数据竞争。

  • 锁定 Mutex:我们通过 .lock() 方法锁定 Mutex,它返回一个 Result。如果锁可用,则成功获取;锁会在作用域结束时自动释放。
  • 恐慌(Panics):如果某个线程在持有锁期间发生 panic,Mutex 将被“污染”(poisoned)。调用 .unwrap() 会确保在锁被污染时程序 panic。

第二步:增加复杂度

现在我们已经有了一个基本示例,接下来增加一些复杂性:我们将引入一个共享的 Vec,并对它执行一些额外的操作。这将为我们提供机会,深入探索在线程之间处理共享可变状态的细节。

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

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

    let mut handles = vec![];

    for i in 0..5 {
        let numbers_clone = Arc::clone(&numbers);
        let handle = thread::spawn(move || {
            let mut num_vec = numbers_clone.lock().unwrap();
            num_vec.push(i);
            println!("Added number: {}", i);
        });

        handles.push(handle);
    }

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

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

此处新增内容:

  • 我们现在有一个在线程之间共享的向量(Vec),每个线程都会向其中添加一个数字。
  • 所有线程完成后,我们打印出向量的最终状态。

解释:

  • 共享的 VecVec 被包裹在 Mutex 中,以确保在修改期间的独占访问。如果没有 Mutex,并发写入会导致数据损坏。
  • 向量安全性:每个线程在向向量添加数字之前都会锁定 Mutex。这确保了没有其他线程能同时修改该向量,从而维护了数据完整性。

挑战与思考

在看过基础示例之后,这里有两个挑战,帮助你加深理解:

挑战 1:

修改代码,使每个线程更新向量中的不同索引。你将如何修改代码,以避免多个线程访问同一索引时发生 panic?(提示:研究 MutexGuard 以及它如何处理并发访问。)

挑战 2:

不要使用简单的计数器,尝试实现一个场景:多个线程对更复杂的数据结构(例如 HashMap)进行递增或修改。你会采用哪些策略来避免竞态条件?


总结与结论

在本文中,我们涵盖了 Rust 中 ArcMutex 的基础用法及一些高级应用场景:

  • Arc 通过原子引用计数,允许多个线程安全地共享数据所有权。
  • Mutex 确保任意时刻只有一个线程可以访问数据,从而防止竞态条件和数据损坏。
  • 我们从一个简单的计数器示例开始,逐步构建到共享 Vec 的场景,并探索了如何在线程之间管理可变的共享状态。

下一步建议:

  • 探索 Rust 中更高级的并发工具,如 RwLock(用于读写锁)和通道(channel)通信。
  • 深入学习 Rust 的异步模型(async model),这是另一种无需阻塞线程即可处理并发的方式。

并发编程可能具有挑战性,但借助 ArcMutex 等工具,Rust 提供了一种健壮的方式来安全地管理线程间的共享数据。

祝你编码愉快!