0Pricing
Learn Rust Coding · 课时

使用 Arc/Mutex 共享状态

安全地共享数据

使用 Arc/Mutex 共享状态 是 CoddyKit 上的免费 Learn Rust Coding 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Learn Rust Coding 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Learn Rust Coding 课程共包含 4 节课。

共享状态为何困难

有时仅靠消息传递还不够,多个线程确实需要读写同一份数据。如果没有保护,Rust 不允许您在线程之间共享可变值,因为这可能导致数据竞争。

您需要结合使用的两个工具是:

  • Arc:在线程之间共享所有权。
  • Mutex:安全地进行独占修改。

Rc 并非线程安全

Rc 提供共享所有权,但只能在单个线程中使用。它的引用计数没有同步机制,因此编译器拒绝在线程之间传递它。要进行多线程共享,您需要使用Arc(原子引用计数)。

Arc 的行为类似于 Rc,但会使用原子操作更新计数,因此可以安全地在线程之间克隆。

use std::sync::Arc;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);
    let clone1 = Arc::clone(&data);
    println!("original: {:?}", data);
    println!("clone:    {:?}", clone1);
    println!("count:    {}", Arc::strong_count(&data));
}

Mutex 提供独占访问

Mutex 会包装数据,并保证同一时间只有一个线程访问它。您可以调用 .lock() 获取一个 MutexGuard,它会解引用到内部值。当保护对象离开其作用域时,锁会自动释放。

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);
    {
        let mut num = m.lock().unwrap();
        *num += 10;
    } // guard dropped here, lock released
    println!("value = {:?}", m.lock().unwrap());
}

组合使用 Arc 和 Mutex

要在线程之间共享可变数据,请将其包装为 Arc>:

  • Arc 让多个线程都拥有指向同一份数据的句柄。
  • Mutex 让每个线程能够安全地依次修改数据。

在线程生成之前,为每个线程克隆一份 Arc。

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let c = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut n = c.lock().unwrap();
        *n += 1;
    });
    handle.join().unwrap();
    println!("counter = {}", *counter.lock().unwrap());
}

多个线程共享一个计数器

一个经典示例是:十个线程分别递增一个共享计数器。每个线程都持有自己的 Arc 克隆,并锁定 Mutex 来加一。加入所有线程后,总数恰好为 10,且不会发生数据竞争。

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    for _ in 0..10 {
        let c = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut num = c.lock().unwrap();
            *num += 1;
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("Result: {}", *counter.lock().unwrap());
}

锁的作用域很重要

MutexGuard 会一直持有锁,直到自身被释放。在执行耗时工作期间持有锁,会阻塞其他线程。请让临界区保持简短:加锁、修改、释放。将锁包装在一个小代码块中,可以确保它在进行额外处理之前就被释放。

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

fn main() {
    let log = Arc::new(Mutex::new(Vec::new()));
    let mut handles = vec![];
    for i in 0..3 {
        let l = Arc::clone(&log);
        handles.push(thread::spawn(move || {
            {
                let mut v = l.lock().unwrap();
                v.push(i);
            } // released quickly
        }));
    }
    for h in handles { h.join().unwrap(); }
    let mut result = log.lock().unwrap().clone();
    result.sort();
    println!("{:?}", result);
}

死锁:真实存在的风险

当两个线程分别持有对方需要的锁,并且双方都永远等待时,就会发生死锁。Rust 可以防止数据竞争,但不能防止死锁。请始终按照相同顺序锁定多个互斥锁,并缩短锁的持有时间,以避免死锁。

此外,也应避免在同一线程中两次锁定同一个 Mutex;标准 Mutex 不支持可重入。

线程发生恐慌时的中毒

如果线程在持有锁时发生恐慌,Mutex 就会被标记为中毒。之后调用 .lock() 会返回 Err,以提醒您数据可能不一致。如果您认为数据仍然可用,可以在错误上调用 into_inner() 来恢复内部保护对象。

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

fn main() {
    let data = Arc::new(Mutex::new(0));
    let d = Arc::clone(&data);
    let _ = thread::spawn(move || {
        let mut g = d.lock().unwrap();
        *g = 7;
        panic!("boom"); // poisons the mutex
    }).join();
    match data.lock() {
        Ok(g) => println!("ok: {}", *g),
        Err(poisoned) => println!("recovered: {}", *poisoned.into_inner()),
    }
}

使用 RwLock 支持多个读取者

当读取次数远多于写入次数时,RwLock 可能比 Mutex 更快。它允许多个读取者同时读取,或允许一个写入者进行写入。使用 .read() 进行共享访问,使用 .write() 进行独占访问。

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

fn main() {
    let config = Arc::new(RwLock::new(String::from("v1")));
    let reader = Arc::clone(&config);
    let r = thread::spawn(move || {
        let val = reader.read().unwrap();
        println!("read: {}", *val);
    });
    r.join().unwrap();
    {
        let mut w = config.write().unwrap();
        *w = String::from("v2");
    }
    println!("final: {}", *config.read().unwrap());
}

使用原子类型处理简单计数器

对于单个整数计数器而言,完整的 Mutex 机制过于复杂。AtomicUsize 等类型可以通过 fetch_add 等方法,在无锁的情况下更新值。将它们包装在 Arc 中即可在线程之间共享。请选择一种 Ordering;SeqCst 是最简单且安全的默认选择。

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

fn main() {
    let counter = Arc::new(AtomicUsize::new(0));
    let mut handles = vec![];
    for _ in 0..5 {
        let c = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            c.fetch_add(1, Ordering::SeqCst);
        }));
    }
    for h in handles { h.join().unwrap(); }
    println!("count = {}", counter.load(Ordering::SeqCst));
}

选择合适的工具

关于共享状态的快速指导:

  • Channel:传递所有权,采用流水线方式。
  • Arc<Mutex>:用于共享的可变结构,以及读写混合的场景。
  • Arc<RwLock>:用于以读取为主的共享数据。
  • Atomics:用于单个基本类型的计数器或标志。

请优先选择能够满足需求的最简单工具;只有在消息传递无法很好地表达问题时,才使用锁。

快速检查

测试您对共享状态的理解。

回顾

您学习了如何安全地在线程之间共享状态:

  • Arc 支持线程安全的共享所有权;Rc 不支持。
  • Mutex 通过保护对象提供独占修改,并会自动解锁。
  • Arc<Mutex<T>> 是共享可变数据的标准模式。
  • 缩短锁的作用域;注意死锁和中毒问题。
  • RwLock 适合以读取为主的数据;原子类型适合简单计数器。

常见问题解答

「使用 Arc/Mutex 共享状态」课时是免费的吗?

是的 — 「使用 Arc/Mutex 共享状态」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Learn Rust Coding 课程的其余内容,请升级到 CoddyKit PRO。 Learn Rust Coding 课程共包含 4 节课。

「使用 Arc/Mutex 共享状态」这节课中我会学到什么?

安全地共享数据 你通过在浏览器中直接运行的动手代码来练习 Learn Rust Coding,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Learn Rust Coding 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Learn Rust Coding 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「使用 Arc/Mutex 共享状态」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Learn Rust Coding 课中编写并运行代码吗?

能。每节 Learn Rust Coding 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. mpsc 通道
  2. 使用 Arc/Mutex 共享状态
  3. 作用域线程
  4. Crossbeam 通道
← 返回 Learn Rust Coding