并发是一个语言及其标准库无法回避的问题,常见的编程语言,到讨论并发之前,都是非常简单易上手的,(当然这点rust有些例外,rust的所有权、引用、生命周期一般是更早的卡点),并发平等的压制着所有语言,让Bug开始出现在初学者的代码中。人也只有一颗脑袋,并发的复杂性并不出人意料。
并发的议题很多,这里分享并发相关的两个频繁出现在标准库中的特征,Send和Sync。
Send和Sync在标准库中的位置是std::marker。
marker里的特征多少带点魔法,意思是这些特征该由哪些类型去实现,都是由编译器去推断或内定的,某个类型不支持某个marker特征,是编译器的约束,根本原因是避免潜在的内存安全问题。
Send
实现Send的类型,才能够在异步环境间进行所有权转移。所说的异步环境,可以是主线程和子线程,也可能是异步运行时的不同Task。标准库中只有少量类型标记为!Send,即不支持Send,例如Rc<T>,Cell<T>系列,可以在marker包中查阅。
在T支持Send的情况下,&T也是支持Send的,不过由于生命周期问题,通常写不出简单的异步传递&T的的代码,生命周期检查不会允许异步随意传递引用,除非这个引用是'static的。通常使用Arc<T>代替&T即可。
Send特征默认有结构传递性,即有Send组合而成的类型也是可Send的。
Sync
提示编译器,实现Sync的类型可以安全地被异步环境进行引用。如果T实现了Sync,那么他的引用&T(或类似引用Box<T>)都是支持Send的(可移交所有权)。更像是对Send进行更底层的一种保证。同样,Sync也是结构传递的,组成Sync的类型也是可Sync的。
一段内存是否可以被异步环境共享,是一种编译器约束,为了杜绝某些可能性而设置的marker。
实现Send或Sync
实现Send或Sync,直接意味着跳过编译器约束,会使潜在问题从编译错误转换成运行时的未定义行为(undefined behavior)。因此,虽然实现这两个特征什么都不用做(特征没有具体方法),但是更需要谨慎对待和思考,其在多线程环境下的表现。
最后放一段多线程执行累加的代码,注意其中Arc和Mutex在异步环境中的使用
let counter = Mutex::new(0);
let arc = Arc::new(counter);
let mut handlers = vec![];
for _ in 0..100 {
let arc_current = arc.clone();
let h = thread::spawn(move||{
let mut local_counter = arc_current.lock().unwrap();
*local_counter += 1;
});
handlers.push(h);
}
for h in handlers {
h.join().unwrap();
}
println!("counter {}", arc.lock().unwrap())