Rust关于并发安全的理念
一个并发场景
我们在使用rust时可能会遇到这样一种使用场景:一份数据要被多个线程共享并且修改,且在此数据使用完毕后需要自动释放内存空间。我们能想到的第一个工具可能是Arc(Atomic reference counting)。Arc用于线程安全地维护一份数据的引用次数,在其所有引用都离开作用域之后,Arc所管理/指向的堆数据将会被回收。只用Arc,我们可以这样做(错误实现):
use std::{sync::Arc, thread::{sleep, spawn}, time::Duration};
fn main() {
let data = Arc::new(5);
{
let mut data = data.clone();
spawn(move || {
let mut_data = Arc::get_mut(&mut data).unwrap();
*mut_data += 1;
});
}
{
let mut data = data.clone();
spawn(move || {
let mut_data = Arc::get_mut(&mut data).unwrap();
*mut_data += 1;
});
}
sleep(Duration::from_millis(1000));
println!("Final data: {}", data);
}
这段程序会panic,Arc get_mut需要有唯一引用,如果已有一个引用的话(let data = Arc::new(5)),get_mut会返回Option::None,进而导致unwrap() panic。
提示:生产环境不要使用unwrap(),使用match语句安全地处理所有情况,避免panic导致程序终止。
那么如何做到并发修改数据呢?如何获取到Arc内数据的可变引用呢?我们可能会想到RefCell(错误实现):
use std::{cell::RefCell, sync::Arc, thread::{sleep, spawn}, time::Duration};
fn main() {
let data = Arc::new(RefCell::new(5));
{
let data = data.clone();
// 不能编译
spawn(move || {
let mut mut_data = data.borrow_mut(); // try_borrow_mut返回Result更好,避免panic
*mut_data += 1;
});
}
{
let data = data.clone();
spawn(move || {
let mut mut_data = data.borrow_mut();
*mut_data += 1;
});
}
sleep(Duration::from_millis(1000));
println!("Final data: {}", data.borrow());
}
以上代码在编译期会报错:
`RefCell<i32>` cannot be shared between threads safely
the trait `Sync` is not implemented for `RefCell<i32>`
if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` instead
required for `Arc<RefCell<i32>>` to implement `Send`
提示RefCell不能被安全地在线程间共享以及RefCell未实现Sync trait。那么到底该如何实现多线程并发修改共享数据呢?
正确使用方式
rust最优秀也是最突出的一点就是它的编译器,它能够给予我们许多有用的信息。在上一节的最后一次尝试后,编译器提示我们使用std::sync::RwLock:
use std::{sync::{Arc, RwLock}, thread::{sleep, spawn}, time::Duration};
fn main() {
let data = Arc::new(RwLock::new(5));
{
let data = data.clone();
spawn(move || {
let mut mut_data = data.write().unwrap();
*mut_data += 1;
});
}
{
let data = data.clone();
spawn(move || {
let mut mut_data = data.write().unwrap();
*mut_data += 1;
});
}
sleep(Duration::from_millis(1000));
println!("Final data: {}", data.read().unwrap());
}
这次终于没有了panic和编译报错,得到了符合预期的结果:
Final data: 7
Rust关于并发安全的理念
Rust在编译期间保证一个数据任一时刻只能被一个线程修改,不能有任何其它线程在读取或写入。我们要做的就是满足所有Rust编译器的“要求”,便能极大地减少并发安全问题(并非全部)。
附1:Send和Sync trait是什么?
Send和Sync是一种标记trait,标记trait的意义大多用来指示编译器进行安全检查。那么Send和Sync保证什么安全呢?当一个内存数据,被多个引用指向并想要修改它时,必须有某种同步机制,可以保证所有的修改,都能够同步地被所有引用知晓,rust通过Send和Sync来表达某个数据类型具有这种机制。
大多数的原始类型都是Send和Sync的,例如整形,浮点型,布尔,字符等。而且Send和Sync是可以自动被编译器实现的,即当结构体包含的所有字段都是Send或Sync时,那么这个结构体也是Send或Sync的。
附2:RefCell有何意义?
RefCell具有Interior mutable的属性。即使是RefCell类型的常量,其内部保存的数据也可修改:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(0);
*data.borrow_mut() += 1;
println!("Data: {}", data.borrow());
}
输出:
Data: 1
这在mock测试场景中可能有价值。例如我们有一个数据Repository特性,其方法定义为fn save(&self, value: &T),之后我们想要mock这个Repository特性作隔离测试,在mock实现中将数据保存至mock结构中的某个“内存数据库”中。但特性是&self引用类型(设计初衷不希望接口的用户改变结构内的字段值,或没有必要修改),意味着其内部数据全部不可变,那么我们如何实现对mock结构中的数据的修改呢?可以使用RefCell:
use std::cell::RefCell;
trait Repository<T> {
fn save(&self, value: T);
}
struct InMemoryRepository<T> {
data: RefCell<Option<T>>,
}
impl<T> Repository<T> for InMemoryRepository<T> {
fn save(&self, value: T) {
if let Ok(mut data) = self.data.try_borrow_mut() {
*data = Some(value);
}
}
}
fn main() {
let repo: InMemoryRepository<i32> = InMemoryRepository {
data: RefCell::new(None),
};
repo.save(42);
println!("Value saved in repository: {:?}", repo.data.borrow());
}
输出:
Value saved in repository: Some(42)
它可以将借用检查从编译器推迟到运行期,但不建议纯粹以此为目的使用。
有些读者此时可能会非常疑惑,那么这不是一种不安全的实现吗?的确是,RefCell是一种妥协,对借用检查的“弱化”。有些时候编译器对于借用的要求过于严格,代码编写起来就会非常困难。理论上在程序员完全相信自己的代码符合借用规则的情况下,才应该使用RefCell来减轻“痛苦”。或者未来的某天,rust编译器进化,借用检查可以覆盖所有的情况,那么这个时候,RefCell就可以退休了。