使用 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 反馈 — 无需本地设置。
此课程中的所有课时
- mpsc 通道
- 使用 Arc/Mutex 共享状态
- 作用域线程
- Crossbeam 通道