Rust 从 0 到 1- 并发 - 可扩展性
有趣的是,Rust 语言自身提供的并发功能很少。我们之前讨论的几乎所有有关并发的内容,都是标准库的一部分,而不是 Rust 语言自身。也正因如此,我们对于并发处理的选择并不受限于标准库或语言自身,我们可以编写自己的并发功能或使用别人编写的。
不过,有两个并发概念是来自语言:std::marker 中的 Sync 和 Send trait。
通过 Send 允许在线程间转移所有权
实现了 Send trait 的类型,其所有权可以在线程间转移。几乎所有的 Rust 类型都实现了 Send ,不过有一些例外,包括 Rc<T>:因为如果我们克隆了 Rc<T> 类型的数据并尝试将其所有权转移到另一个线程,那么这两个线程可能在“同一时间”更新引用计数。为此,Rust 提供了两种实现,Rc<T> 用于单线程场景,不需要为保证线程安全而付出性能代价;而 Arc<T> 用于多线程场景。
Rust 类型系统和 trait bound 确保了我们永远也不可能将线程不安全的 Rc<T> 用于多线程场景。我们在前面的例子中曾经尝试过,编译器提示了我们错误 “the trait Send is not implemented for Rc<Mutex<i32>>”,而换成实现了 Send 的 Arc<T> 时,就没问题了。
此外,任何完全由实现了 Send 的类型组成的类型会自动被标记 Send。几乎所有基本类型都实现了 Send,除了后面我们将会讨论的 raw pointer(原始指针或裸指针)。
通过 Sync 允许多线程访问
实现了 Sync trait 的类型可以安全的在多个线程中引用。从另一方面说,对于任意类型 T,如果 &T(T 的不可变引用)是 Send 的话 T 就是 Sync 的,也就是说如果其引用可以安全的发送到另一个线程,那么它就是 Sync 的,可以安全的在多个线程中并发访问。和 Send 类似,基本类型都是 Sync 的,完全由 Sync 的类型组成的类型也是 Sync 的。
智能指针 Rc<T> 也不是 Sync 的,和它不是 Send 的原因相同。RefCell<T> 以及 Cell<T> 系列类型也都不是 Sync 的。RefCell<T> 实现的运行时借用检查是线程不安全的。Mutex<T> 是 Sync 的,就像我们在前面例子中演示的,它可以由多个线程并发访问。
手动实现 Send 和 Sync 是不安全的
由于由 Send 和 Sync 的类型组成的类型,自动就是 Send 和 Sync (Rust 会自动推断),我们并不需要手动实现 Send 和 Sync trait。因为他们是 marker trait(标记),甚至都不需要为它们实现方法。他们只是用于检查是否可用于并发。
手动实现这些 marker trait 涉及到编写不安全的 Rust 代码,在后面章节我们会介绍;目前,在创建新的用于并发的类型时,如果其不全部是由标记了 Send 和 Sync 的类型构成,需要多加小心,确保其是并发安全的。如果大家比较感兴趣,建议大家进一步阅读官方文档“The Rustonomicon ”。
总结
正如前面提到的,因为 Rust 本身提供了很少的处理并发的功能,有很多的并发方案都是由社区共享的 crates 实现,他们发展的比标准库更快;我们可以在网上搜索当前最好的用于处理多线程场景的 crates。
Rust 标准库提供了用于消息传递的 channel,和像 Mutex<T> 和 Arc<T> 这类并发安全的智能指针。类型系统和借用检查器会确保在并发场景下使用它们不会出现数据竞争(data race)和无效引用。只要代码可以编译通过,我们就可以放心的在多线程场景下运行它们,而不会出现在其它语言中出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:不用再害怕它,大胆勇敢的编写并发程序吧!
接下来,我们将讨论当 Rust 程序规模变得更大时,对于问题建模和结构组织的习惯做法,以及这些与面向对象编程(Object Oriented Programming)概念的联系。
版权声明: 本文为 InfoQ 作者【山】的原创文章。
原文链接:【http://xie.infoq.cn/article/74e5b1fa6a6202f63984a96af】。文章转载请联系作者。
评论