我试图创建一个线程安全的数据类型,其中的锁是完全封装的,这样调用者就不会看到或直接持有一个锁(这部分工作正常)。
此外,还要求生成该数据类型示例的结构体保存一个可变引用,并向调用方发出一个可变引用(这就是我遇到的问题)。
下面的示例代码很好地解决了这个问题:
use std::sync::{Arc, RwLock};
use std::cell::RefCell;
trait ChangeName {
fn change_name(&mut self, name: String);
}
// MyDataPrivate contains the data I care about.
#[derive(Debug)]
struct MyDataPrivate {
name: String,
}
impl ChangeName for MyDataPrivate {
fn change_name(&mut self, name: String) {
self.name = name;
}
}
// MyData encapsulates locking around MyData.
// Callers never hold a lock directly.
#[derive(Debug)]
pub struct MyData {
inner: RwLock<MyDataPrivate>,
}
impl ChangeName for MyData {
fn change_name(&mut self, name: String) {
self.inner.write().unwrap().change_name(name);
}
}
impl MyData {
fn new(name: String) -> Self {
Self {
inner: RwLock::new(MyDataPrivate{name})
}
}
}
// MyDataRef provides mutable references to MyData.
#[derive(Debug, Clone)]
pub struct MyDataRef(Arc<RefCell<MyData>>);
impl ChangeName for MyDataRef {
fn change_name(&mut self, name: String) {
self.0.borrow_mut().change_name(name);
}
}
// This makes it build. Is it Safe/Ok since MyData is Send + Sync?
unsafe impl Sync for MyDataRef {}
unsafe impl Send for MyDataRef {}
fn do_stuff<T: Sync + Send>(_t: T) {}
fn main() {
let mydata = MyData::new("Donner".to_string());
let mut mydata_ref = MyDataRef(Arc::new(RefCell::new(mydata)));
do_stuff(mydata_ref.clone());
mydata_ref.change_name("Rudolf".to_string());
println!("name: {:?}", mydata_ref);
}
字符串
请参阅Playground。
如果不使用unsafe
到impl Send + Sync for MyDataRef
,上面的代码将不会编译。这是因为RefCell
不是线程安全的。
然而,我放在RefCell
中的数据是线程安全的(受RwLock
保护),所以我认为这也使得这个特定的RefCell
是线程安全的。
我意识到我可以在MyDataRef
中使用Arc<RwLock<MyData>>
,而不是Arc<RefCell<MyData>>
,但是我在MyDataPrivate
周围有两个RwLock
,这似乎是不必要的,也很恶心。
我的问题分为两部分:
1.在impl Send + Sync
所包含的数据受到100% RwLock
保护的情况下,将impl Send + Sync
替换为RefCell
是否安全/正确?
2)有没有更好的方法来做到这一点,仍然保持锁定封装远离调用方?
编辑:我算出了a nice solution。
ps:在原代码中MyDataRef
实际上是一个类型别名,例如:
pub type MyDataRef = Arc<RefCell<MyData>>;
型
在这个例子中,我将MyDataRef
改为newtype,这样我就可以在上面使用unsafe impl Send + Sync
了。
2条答案
按热度按时间9w11ddsr1#
就像写的那样,我不认为这是一种声音。
在Rust中,未定义的行为只能发生在
unsafe
块中。这个impl
违反了这个预期:字符串
不能同时从多个线程调用此函数,但不能静态保证不会这样。如果您想将此负担转移到调用者身上,此函数必须标记为
unsafe
,并记录安全限制。如果不将trait方法本身标记为unsafe
,然后 all,则无法做到这一点实现需要是unsafe
,即使它们本来不需要。我不认为您的示例代码调用了任何未定义的行为,因为您在尝试从
RefCell
借用之前采用了排他锁,但实际上并没有令人信服的理由来说明您首先需要以这种方式构造数据。你在这里实际上是
RwLock<Arc<RefCell<_>>>
,这种嵌套 * 绝对没有意义。* 你在线程安全的内部可变性中有非线程安全的内部可变性,在线程安全的内部可变性中有线程安全的共享所有权。它混合在一起的东西与冲突的线程安全性,并以一种无用的顺序。(你可以说它没有意义,因为你必须调用unsafe
才能做你想做的事情!如果数据模型引起了大量的摩擦,那么它可能不是正确的模型。RwLock<Arc<T>>
* 可以 * 当你想用另一个Arc
1替换一个Arc
时很有用,但是当你想在Arc
中改变T
时就没有用了。Arc<RefCell<T>>
完全没有意义,因为您正在将非线程安全的内部可变性类型 Package 在线程安全的共享所有权类型中。相反,用途:
Rc<RefCell<_>>
,如果您需要具有内部可变性的单线程共享所有权,或者Arc<Mutex<_>>
或Arc<RwLock<_>>
,如果您需要具有内部可变性的多线程共享所有权。Arc<Mutex<_>>
在您总是或几乎总是需要独占访问时是有意义的。Arc<RwLock<_>>
在你几乎总是需要共享访问,但偶尔需要独占访问时是有意义的。特别注意,std::sync::RwLock
不能保证是“公平的”,这意味着理论上大量的读取活动可以无限期地饿死写入者。[1]
arc-swap
crate在概念上实现了与RwLock<Arc<T>>
相同的东西,但性能更好。有时,您需要能够原子地更新共享数据结构,但不会中断其他仍在使用旧值的线程。这种所有权模型允许一种“快照”,任务将看到旧数据或新数据,但绝不会看到两者的混合。yshpjwxd2#
不,
RefCell
never 线程安全(特别是它从来没有实现Sync
),因为它有内部计数器用于可变和不可变的借用,这些借用是非原子地递增的。如果你被允许在不同的线程上有两个对同一个RefCell
的引用,并且它们都试图同时借用或释放一个借用,它们可能在数据竞争中结束,其中计数器同时递增或递减,这可能破坏它们并导致例如绕过运行时检查并允许冲突借用,这是未定义的行为。对于可以跨线程访问的
RefCell
版本,您需要改用RwLock
。