Rust多线程中如何实现共享内存的可变与不可变访问
跨线程读写共享资源的Rust实现问题
我有一个多线程程序,需要跨线程访问资源:部分线程写入资源,部分线程读取资源。不确定这是否属于全局可变单例场景(我的设置并非全局,但解决方案可能类似)。
以下是简化后的代码示例,目标是让一个线程写入结构体,另一个线程读取同一结构体,确保写入后读取线程能获取到最新数据:
use std::{thread, time::Duration}; struct TagList { list: Vec<String> } impl TagList { fn add(self: &mut TagList, tag: String) { self.list.push(tag); } fn read(&self) -> Vec<String> { self.list.clone() } } fn main() { let mut list = TagList { list: vec![] }; thread::spawn(move || { ["fee", "foo", "faa", "fuu"].into_iter().for_each(|tag| { list.add(tag.to_string()); thread::sleep(Duration::from_millis(100)); }); }); thread::spawn(move || { loop { dbg!(list.read()); thread::sleep(Duration::from_millis(20)); } }); }
这段代码触发了借用错误:
error[E0382]: use of moved value: `list` --> src/main.rs:79:19 | 70 | let mut list = TagList { list: vec![] }; | -------- move occurs because `list` has type `TagList`, which does not implement the `Copy` trait 71 | 72 | thread::spawn(move || { | ------- value moved into closure here 73 | ["fee", "foo", "faa", "fuu"].into_iter().for_each(|tag| { 74 | list.add(tag.to_string()); | ---- variable moved due to use in closure ... 79 | thread::spawn(move || { | ^^^^^^^ value used here after move 80 | dbg!(list.read()); | ---- use occurs due to use in closure
我尝试用Arc包裹列表解决问题:
use std::sync::Arc; // ... let list = Arc::new(TagList { list: vec![] }); let write_list = Arc::get_mut(&mut list).unwrap(); let read_list = Arc::clone(&list); thread::spawn(move || { ["fee", "foo", "faa", "fuu"].into_iter().for_each(|tag| { write_list.add(tag.to_string()); thread::sleep(Duration::from_millis(100)); }); }); thread::spawn(move || { loop { dbg!(read_list.read()); thread::sleep(Duration::from_millis(20)); } });
但依然报错,显然我没理解Arc的工作原理和生命周期关系:
error[E0597]: `list` does not live long enough --> src/main.rs:71:35 | 71 | let write_list = Arc::get_mut(&mut list).unwrap(); | -------------^^^^^^^^^- | | | | | borrowed value does not live long enough | argument requires that `list` is borrowed for `'static` ... 85 | } | - `list` dropped here while still borrowed error[E0502]: cannot borrow `list` as immutable because it is also borrowed as mutable --> src/main.rs:72:32 | 71 | let write_list = Arc::get_mut(&mut list).unwrap(); | ----------------------- | | | | | mutable borrow occurs here | argument requires that `list` is borrowed for `'static` 72 | let read_list = Arc::clone(&list); | ^^^^^ immutable borrow occurs here
我的疑问:
- 这个功能是否可行?(我知道
std::sync::mpsc能跨线程推送读取消息,类似功能应该存在) - 应该使用什么工具?
Arc是合适的选择吗,还是找错了方向? - 需要学习哪些内容才能理解Rust中这类问题的典型解决方法?
解决方案与解释
功能完全可行
这类需求是典型的多线程共享可变资源场景,Rust标准库提供了成熟的工具实现,和mpsc属于不同的线程通信模式(mpsc是消息传递,这里是共享内存)。
为什么之前的Arc用法错误
Arc的作用是提供线程安全的引用计数,让多个线程能持有同一个资源的所有权,但它本身不提供内部可变性,也不允许同时可变访问。Arc::get_mut获取可变引用的前提是当前只有一个Arc实例存在,一旦克隆Arc,这个方法就会返回None;而且你拿到的可变引用绑定到原Arc的生命周期上,无法安全传递到线程中(线程要求捕获的变量生命周期为'static)。
正确的工具组合:Arc + 同步原语
要实现多线程下的读写,需要结合Arc和以下同步原语之一:
1. Arc<Mutex<TagList>>:互斥锁
适用于读写频率相近或写操作频繁的场景,同一时间只能有一个线程获取锁(无论读还是写)。
示例代码:
use std::{thread, time::Duration, sync::{Arc, Mutex}}; struct TagList { list: Vec<String> } impl TagList { fn add(&mut self, tag: String) { self.list.push(tag); } fn read(&self) -> Vec<String> { self.list.clone() } } fn main() { let list = Arc::new(Mutex::new(TagList { list: vec![] })); // 克隆Arc给写线程 let write_list = Arc::clone(&list); thread::spawn(move || { ["fee", "foo", "faa", "fuu"].into_iter().for_each(|tag| { // 获取锁,unwrap忽略锁被poisoned的情况(生产环境需处理) let mut guard = write_list.lock().unwrap(); guard.add(tag.to_string()); // 锁在guard离开作用域时自动释放 thread::sleep(Duration::from_millis(100)); }); }); // 克隆Arc给读线程 let read_list = Arc::clone(&list); thread::spawn(move || { loop { let guard = read_list.lock().unwrap(); dbg!(guard.read()); thread::sleep(Duration::from_millis(20)); } }); // 主线程等待子线程完成,否则主线程退出会直接终止所有子线程 thread::sleep(Duration::from_millis(500)); }
2. Arc<RwLock<TagList>>:读写锁
适用于读多写少的场景,允许多个读线程同时获取读锁,同一时间只能有一个写线程获取写锁,写锁会阻塞所有读锁和其他写锁。
示例代码:
use std::{thread, time::Duration, sync::{Arc, RwLock}}; struct TagList { list: Vec<String> } impl TagList { fn add(&mut self, tag: String) { self.list.push(tag); } fn read(&self) -> Vec<String> { self.list.clone() } } fn main() { let list = Arc::new(RwLock::new(TagList { list: vec![] })); let write_list = Arc::clone(&list); thread::spawn(move || { ["fee", "foo", "faa", "fuu"].into_iter().for_each(|tag| { // 获取写锁 let mut guard = write_list.write().unwrap(); guard.add(tag.to_string()); thread::sleep(Duration::from_millis(100)); }); }); let read_list = Arc::clone(&list); thread::spawn(move || { loop { // 获取读锁 let guard = read_list.read().unwrap(); dbg!(guard.read()); thread::sleep(Duration::from_millis(20)); } }); thread::sleep(Duration::from_millis(500)); }
需要学习的核心内容
- Rust的所有权与借用规则:这是理解线程安全问题的基础,初始错误就是所有权转移导致的。
- 线程安全与
Send/Synctrait:Rust通过这两个trait标记类型是否能安全跨线程传递或共享,Arc、Mutex、RwLock都实现了这两个trait。 - 共享内存的同步原语:除了
Mutex和RwLock,还可了解Condvar(条件变量)等工具,用于更复杂的线程同步场景。 - 内部可变性:
RefCell是单线程的内部可变性工具,Mutex/RwLock是线程安全的内部可变性工具,结合Arc就能实现多线程共享可变资源。
内容的提问来源于stack exchange,提问作者berkes
相关产品推荐
相关产品推荐

