无锁并发结构体字段使用:多线程修改后整体复用实现方案
问题描述
假设定义了如下结构体:
struct MyStruct { field1: Box<MyField>, field2: Box<MyField>, }
需要实现以下操作:
- 在不同线程中以可变方式使用结构体的各个字段;
- 等待所有线程操作完成;
- 之后将该结构体作为整体使用。
理论上引用计数类型(如Arc)即可满足需求(无需锁),以下是一段存在错误的示例代码:
// Function to call when everything's done. fn all_done(s: MyStruct) { ... } // Holds MyStruct until all references are dropped and then calls `all_done` struct Guard(Option<MyStruct>); impl Guard { fn new(s: MyStruct) -> Self { Self(Some(s)) } } impl Drop for Guard { fn drop(&mut self) { all_done(self.0.take().unwrap()); } } fn main() { let mut s: MyStruct = ...; let f1 = &mut s.field1; let f2 = &mut s.field2; let s = Arc::new(Guard::new(s)); let s_copy = s.clone(); thread::spawn(move || { do_some_mutating_with(f1); drop(s_copy); }); thread::spawn(move || { do_some_mutating_with(f2); drop(s); }); }
注意点
- 引用不会超过值的生命周期,线程持有该值的
Arc实例可保证这一点; - 结构体的任何部分都不会在多线程中被并发使用。
编译器报错原因
Arc::new(Guard::new(s))移动了已被借用的对象;若一开始就在Arc中创建该对象,则会遇到“cannot borrow data in an Arc as mutable”的问题,且当前引用指向栈上值的方式并不恰当。- 编译器无法确定引用的生命周期足够长。
请问是否有重构代码的方式实现预期功能?若没有,是否有可实现等效功能的库?
解决方案
标准库作用域线程(推荐,无unsafe无依赖)
Rust 1.63+稳定版支持作用域线程,编译器能自动检查引用生命周期,无需Arc或unsafe即可安全实现需求:
use std::thread; struct MyField; struct MyStruct { field1: Box<MyField>, field2: Box<MyField>, } fn do_some_mutating_with(f: &mut Box<MyField>) { // 字段可变操作逻辑 } fn all_done(s: MyStruct) { // 最终结构体处理逻辑 } fn main() { let mut s = MyStruct { field1: Box::new(MyField), field2: Box::new(MyField), }; // 作用域线程自动保证引用生命周期安全 thread::scope(|scope| { // 线程1处理field1 scope.spawn(|| { do_some_mutating_with(&mut s.field1); }); // 线程2处理field2 scope.spawn(|| { do_some_mutating_with(&mut s.field2); }); }); // 此处自动等待所有线程完成 // 所有线程处理完毕,使用完整结构体 all_done(s); }
这个方案利用作用域线程的特性,让编译器确认线程中的引用不会超过结构体的生命周期,同时因为两个线程操作不同字段,不存在可变引用冲突,完全符合Rust安全规范。
UnsafeCell+Arc方案(手动保证安全)
如果需要脱离作用域线程的限制,可以用UnsafeCell配合Arc实现,手动保证无并发访问的前提下,unsafe操作是安全的:
use std::sync::Arc; use std::cell::UnsafeCell; use std::thread; struct MyField; struct MyStruct { field1: Box<MyField>, field2: Box<MyField>, } fn do_some_mutating_with(f: &mut Box<MyField>) { // 字段可变操作逻辑 } fn all_done(s: MyStruct) { // 最终结构体处理逻辑 } fn main() { let mut s = MyStruct { field1: Box::new(MyField), field2: Box::new(MyField), }; // 拆分字段并放入Arc<UnsafeCell> let field1 = Arc::new(UnsafeCell::new(s.field1)); let field2 = Arc::new(UnsafeCell::new(s.field2)); // 启动线程处理field1 let field1_clone = field1.clone(); let handle1 = thread::spawn(move || { // 手动保证无并发访问,unsafe操作安全 let f = unsafe { &mut *field1_clone.get() }; do_some_mutating_with(f); }); // 启动线程处理field2 let handle2 = thread::spawn(move || { let f = unsafe { &mut *field2.get() }; do_some_mutating_with(f); }); // 等待所有线程完成 handle1.join().unwrap(); handle2.join().unwrap(); // 重新组装结构体 let final_struct = MyStruct { field1: unsafe { Arc::try_unwrap(field1).unwrap().into_inner() }, field2: unsafe { Arc::try_unwrap(field2).unwrap().into_inner() }, }; all_done(final_struct); }
原方案不可行的核心原因
原方案违反了Rust的借用规则:当存在字段的可变引用时,原结构体不能被移动;若先将结构体放入Arc,又无法直接获取可变引用(Arc默认仅提供共享不可变访问)。作用域线程或UnsafeCell+Arc的方式,分别通过编译器检查或手动安全保证,绕开了这个限制。
内容的提问来源于stack exchange,提问作者Avi
相关产品推荐
相关产品推荐

