做者:谢敬伟,江湖人称“刀哥”,20年IT老兵,数据通讯网络专家,电信网络架构师,目前任Netwarps开发总监。刀哥在操做系统、网络编程、高并发、高吞吐、高可用性等领域有多年的实践经验,并对网络及编程等方面的新技术有浓厚的兴趣。程序员
现代的CPU
基本都是多核结构,为了充分利用多核的能力,多线程都是绕不开的话题。不管是同步或是异步编程,与多线程相关的问题一直都是困难而且容易出错的,本质上是由于多线程程序的复杂性,特别是竞争条件的错误,使得错误发生具有必定的随机性,而随着程序的规模愈来愈大,解决问题的难度也随之愈来愈高。编程
其余语言的作法
C/C++
将同步互斥,以及线程通讯的问题所有交给了程序员。关键的共享资源通常须要经过Mutex/Semaphone/CondVariable之类的同步原语保证安全。简单地说,就是须要加锁。然而怎么加,在哪儿加,怎么释放,都是程序员的自由。不加也能跑,绝大多数时候,也不会出问题。当程序的负载上来以后,不经意间程序崩溃了,而后就是痛苦地寻找问题的过程。安全
Go
提供了经过channel
的消息机制来规范化协程之间的通讯,可是对于共享资源,作法与C/C++
没有什么不一样。固然,遇到的问题也是相似。微信
Rust 作法
与Go
相似,Rust
也提出了channel
机制用于线程之间的通讯。由于Rust
全部权的关系,没法同时持有多个可变引用,所以channel
被分红了rx
和tx
两部分,使用起来没有Go
的那么直观和顺手。事实上,channel
的内部实现也是使用原子操做、同步原语对于共享资源的封装。因此,问题的根源依然在于Rust
如何操做共享资源。 Rust
经过全部权以及Type
系统给出了解决问题的一个不一样的思路,共享资源的同步与互斥再也不是程序员的选项,Rust
代码中同步及互斥相关的并发错误都是编译时错误,强迫程序员在开发时就写出正确的代码,这样远远好过面对在生产环境中顶着压力排查问题的窘境。咱们来看一看这一切是如何作到的。网络
Send,Sync 到底是什么
Rust
语言层面经过 std::marker
提供了 Send
和 Sync
两个Trait
。通常地说法,Send
标记代表类型的全部权能够在线程间传递,Sync
标记代表一个实现了Sync
的类型能够安全地在多个线程中拥有其值的引用。这段话很费解,为了更好地理解Send
和 Sync
,须要看一看这两个约束到底是怎样被使用的。如下是标准库中std::thread::spawn()
的实现:数据结构
pub fn spawn<F, T>(self, f: F) -> io::Result<JoinHandle<T>> where F: FnOnce() -> T, F: Send + 'static, T: Send + 'static, { unsafe { self.spawn_unchecked(f) } }
能够看到,建立一个线程,须要提供一个闭包,而这个闭包的约束是 Send
,也就是须要能转移到线程中,闭包返回值T
的约束也是 Send
(这个不难理解,线程运行后返回值须要转移回去) 。举例说明,如下代码没法经过编译。多线程
let a = Rc::new(100); let h = thread::spawn(move|| { let b = *a+1; }); h.join();
编译器指出,std::rc::Rc<i32>
cannot be sent between threads safely。缘由在于,闭包的实如今内部是由编译器建立一个匿名结构,将捕获的变量存入此结构。以上代码闭包大体被翻译成:闭包
struct { a: Rc::new(100), ... }
而Rc<T>
是不支持 Send
的数据类型,所以该匿名结构,即这个闭包,也不支持 Send
,没法知足std::thread::spawn()
关于F
的约束。架构
上面代码改用Arc<T>
,则编译经过,由于Arc<T>
是一种支持 Send
的数据类型。可是Arc<T>
不容许共享可变引用,若是想实现多线程之间修改共享资源,则须要使用Mutex<T>
来包裹数据。代码会改成这个样子:并发
let mut a = Arc::new(Mutex::new(100)); let h = thread::spawn(move|| { let mut shared = a.lock().unwrap(); *shared = 101; }); h.join();
为何Mutex<T>
能够作到这一点,可否改用RefCell<T>
完成相同功能?答案是否认的。咱们来看一下这几个数据类型的限定:
unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {} unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {} unsafe impl<T: ?Sized> Send for RefCell<T> where T: Send {} impl<T: ?Sized> !Sync for RefCell<T> {} unsafe impl<T: ?Sized + Send> Send for Mutex<T> {} unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
Arc<T>
能够Send
,当其包裹的T
同时支持Send
和Sync
。很明显Arc<RefCell<T>>
不知足此条件,由于RefCell<T>
不支持Sync
。而Mutex<T>
在其包裹的T
支持Send
的前提下,知足同时支持Send
和Sync
。实际上,Mutex<T>
的做用就是将一个支持Send
的普通数据结构转化为支持Sync
,进而能够经过Arc<T>
传入线程中。咱们知道,多线程下访问共享资源须要加锁,因此Mutex::lock()
正是这样一个操做,lock()
以后便获取到内部数据的可变引用。 经过上述分析,咱们看到Rust
另辟蹊径,利用全部权以及Type
系统在编译时刻解决了多线程共享资源的问题,的确是一个巧妙的设计。
异步代码,协程
异步代码同步互斥问题与同步多线程代码没有本质不一样。异步运行库通常提供相似于std::thread::spawn()
的方式来建立协程/任务,如下是async-std
建立一个协程/任务的API
:
pub fn spawn<F, T>(future: F) -> JoinHandle<T> where F: Future<Output = T> + Send + 'static, T: Send + 'static, { Builder::new().spawn(future).expect("cannot spawn task") }
能够看到,与std::thread::spawn()
很是类似,闭包换成了Future
,而Future
要求Send
约束。这意味着参数future
必须能够Send
。咱们知道,async
语法经过generaror
生成了一个状态机驱动的Future
,而generaror
与闭包相似,捕获变量,放入一个匿名数据结构。因此这里变量必须也是Send
才能知足Future
的Send
约束条件。试图转移一个Rc<T>
进入async block
依然会被编译器拒绝。如下代码没法经过编译:
let a = Rc::new(100); let h = task::spawn(async move { let b = a; });
此外,在异步代码中,原则上应当避免使用同步的操做从而影响异步代码的运行效率。试想一下,若是Future
中调用了std::mutex::lock
,则当前线程被挂起,Executor
将再也不有机会执行其余任务。为此,异步运行库通常提供了相似于标准库的各类同步原语。这些同步原语不会挂起线程,而是当没法获取资源时返回Poll::Pending
,Executor
将当前任务挂起,执行其余任务。
完美了么?死锁问题
Rust
虽然用一种优雅的方式解决了多线程同步互斥的问题,但这并不能解决程序的逻辑错误。所以,多线程程序最使人头痛的死锁问题依然会存在于Rust
的代码中。因此说,所谓Rust
“无惧并发”是有前提的。至少在目前,看不到编译器能够智能到分析并解决人类逻辑错误的水平。固然,届时程序员这个岗位应该也就不存在了...
深圳星链网科科技有限公司(Netwarps),专一于互联网安全存储领域技术的研发与应用,是先进的安全存储基础设施提供商,主要产品有去中心化文件系统(DFS)、区块链基础平台(SNC)、区块链操做系统(BOS)。 微信公众号:Netwarps