分析tokio源码引发的引用与并发的关系思考
背景
今天在分析tokio =>tokio_test => src => task.rs
中下面这段代码时,发现一个有趣的现象:
impl ThreadWaker {
fn new() -> Self {
ThreadWaker {
state: Mutex::new(IDLE),
condvar: Condvar::new(),
}
}
/// Clears any previously received wakes, avoiding potential spurious
/// wake notifications. This should only be called immediately before running the
/// task.
fn clear(&self) {
*self.state.lock().unwrap() = IDLE;
}
在clear
方法中,对不可变借用的self可以改变里面的值,平时在用Mutex的时候只是在用,知道可以lock后可以修改其值,但是没有仔细去思考其中缘由。说到这里,其实我们会很自然的想到一个问题。
问题1: 既然想改变数据,为什么不明确传递&mut self呢?
Rust 中传递 &self
和 &mut self
的区别在于所有权和借用的规则。下面我将详细解释为什么在某些情况下,即使想改变数据,也可能选择传递 &self
而不是 &mut self
。
&self
vs &mut self
&self
: 表示对self
的不可变引用。通常用在不需要改变自身状态的方法中。&mut self
: 表示对self
的可变引用。用于需要改变自身状态的方法中。
内部可变性(Interior Mutability)
在 Rust 中,内部可变性(Interior Mutability)是一种设计模式,它允许你在通过不可变引用访问对象时修改其内部状态。这种模式通常通过类型系统中的某些类型来实现,如 RefCell<T>
和 Mutex<T>
。这些类型允许你在持有不可变引用的情况下进行修改。
这就很好解释了为什么只需要不可变引用了。
但是,
问题2: 使用&self的好处是什么呢?
使用 &self
而不是 &mut self
修改数据的原因通常包括以下几点:
- 并发访问:
- 当你需要在多线程环境中访问和修改数据时,使用
Mutex
是一个常见的模式。通过使用Mutex
,你可以确保即使多个线程同时访问数据,也能保持线程安全。而且你可以通过不可变引用&self
来获取Mutex
的锁并修改数据,而不需要可变引用。
- 当你需要在多线程环境中访问和修改数据时,使用
- API 的灵活性:
- 允许在方法中使用不可变引用
&self
提供了更大的灵活性。用户可以在持有对象的不可变引用时调用这些方法,而不需要担心可变借用的生命周期问题。
- 允许在方法中使用不可变引用
- 内部实现细节隐藏:
- 使用内部可变性模式,你可以隐藏对象内部状态变化的细节,对外暴露一个更简洁的 API。例如,外部代码不需要知道你的对象内部使用了
Mutex
或RefCell
,只需要知道可以通过不可变引用来调用方法。
- 使用内部可变性模式,你可以隐藏对象内部状态变化的细节,对外暴露一个更简洁的 API。例如,外部代码不需要知道你的对象内部使用了
问题3: 那就是说,&mut其实本身强调同一时间只能有一个可修改的借用,用完后还回来其他地方才能继续可变借用走。这样就限制了并发吧?
是的,准确地说,&mut
确实强调同一时间只能有一个可修改的借用,这与 Rust 的所有权和借用规则密切相关。这些规则旨在确保内存安全和数据一致性。具体来说:
&mut
借用的特点
- 独占性:
&mut
借用是独占的。在同一时间点,只能有一个可变引用。这意味着在持有&mut
借用期间,不能有其他任何借用(无论是可变还是不可变)。 - 可变性:
&mut
借用允许对数据进行修改。
这种设计的初衷是为了避免数据竞争和内存安全问题。在并发环境下,多个线程同时访问和修改同一数据可能导致数据竞争,而 Rust 通过严格的借用规则防止了这一点。
Mutex
和内部可变性
当需要在多线程环境中安全地修改数据时,使用 Mutex
或其他内部可变性类型是一种解决方案。这些类型允许在保持数据所有权和借用规则不变的前提下,实现对数据的安全并发修改。
具体示例
考虑以下代码示例,它展示了使用 &mut
和 Mutex
的区别:
使用 &mut
struct Data {
value: i32,
}
impl Data {
fn set_value(&mut self, new_value: i32) {
self.value = new_value;
}
}
fn main() {
let mut data = Data { value: 0 };
data.set_value(42);
}
在这个示例中,set_value
方法需要 &mut self
,这意味着在调用 set_value
方法期间,data
不能有其他借用。这限制了数据在并发场景中的修改,因为不能同时有多个线程持有 &mut
借用。
使用 Mutex
use std::sync::{Arc, Mutex};
use std::thread;
struct Data {
value: Mutex<i32>,
}
impl Data {
fn set_value(&self, new_value: i32) {
let mut value = self.value.lock().unwrap();
*value = new_value;
}
}
fn main() {
let data = Arc::new(Data { value: Mutex::new(0) });
let mut handles = vec![];
for i in 0..10 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
data.set_value(i);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final value: {:?}", data.value.lock().unwrap());
}
在这个并发示例中,多个线程可以同时调用 set_value
方法,因为每个线程都会获取 Mutex
的锁,从而保证数据修改的安全性和一致性。
那么问题又来了
问题3: Mutex 怎么保证线程间和线程内部数据安全和内部可变性的呢?
Mutex
通过实现 Send
和 Sync
这两个 trait 来保证在线程之间安全地传递和共享数据,从而实现内部可变性。下面是更详细的解释:
Send
和 Sync
Trait
Send
: 允许类型的所有权在线程间转移。如果一个类型实现了Send
,那么它的实例可以安全地在线程间移动。Sync
: 允许类型的引用在多个线程中共享。如果一个类型实现了Sync
,那么对该类型的引用可以安全地在多个线程中共享。
Mutex
的实现
在 Rust 标准库中,Mutex
实现了 Send
和 Sync
,确保其在线程之间传递和共享时的安全性:
Mutex<T>
实现Send
,前提是T
也实现了Send
。Mutex<T>
实现Sync
,前提是T
也实现了Sync
。
通过实现这两个 trait,Mutex
可以在线程间安全地传递,并允许多个线程安全地访问和修改被保护的数据。
内部可变性
由于 Mutex
提供了内部可变性,即使通过不可变引用 &self
,也可以安全地修改内部的数据。这是通过锁机制实现的,确保同一时间只有一个线程可以访问被保护的数据,从而避免数据竞争和不一致性。
总结
其实,这种设计模式在 Rust 中非常常见,特别是在需要内部可变性和线程安全的场景下使用 Mutex
,RwLock
等类型。
本作品采用《CC 协议》,转载必须注明作者和本文链接