📜  Rust——安全并发的一个案例

📅  最后修改于: 2022-05-13 01:55:00.955000             🧑  作者: Mango

Rust——安全并发的一个案例

在研究 Rust 本身之前,让我们回到 1992 年。Guido van Rossum 为了解决 CPython 解释器中的竞争条件,添加了一个著名的锁,称为 Global Interpreter Lock 或简称 GIL。两年半之后,这是我们都喜欢和使用的Python解释器的主要缺点之一。

那么什么是全局解释器锁?

在Python中,您创建的所有内容都作为Python对象分配到内存中,并返回对它的引用。让我们使用视觉效果来更好地了解正在发生的事情。考虑以下代码行:

a = []

在幕后,这就是Python解释器所做的

锈

[] 的空间被分配,它的引用返回给 A。如果我们将 a 分配给另一个变量 b 会发生什么?

a = []
b = a

如果我们从窗帘后面偷看,这就是它的样子:

锈

删除所有引用时如何释放内存?

这就是 Python 的简单性发挥作用的地方。与Python对象相关的另一个值称为引用计数。引用计数是一个数字,它告诉有多少变量持有对给定分配值的引用。当进行新的参考时,该值会增加。当引用被删除时,该值会递减。为了使上面的图表更清楚,这就是它们在引用计数下的样子。

锈

锈

当引用计数降至零时,分配给对象的内存被释放,这就是 CPython 解释器管理内存的方式。无需定期运行任何垃圾收集器,它使 C API 与Python的集成变得轻而易举。

注意:更多信息,请参阅什么是Python全局解释器锁 (GIL)

随之而来的是一个很大的限制——如果两个线程想要创建一个新的引用或删除对一个对象的引用怎么办?

以变量 a 和 b 为例。如果 a 和 b 在不同的线程上并且想同时删除引用,它会创建一个称为竞争条件的东西。假设首先读取、递减和存储引用计数——这就是汇编代码中发生的事情。如果读取发生在完全相同的时间,则两个线程都将取值 2,将其递减为 1,然后将其写回对象。这里的问题是两个引用都被删除了,但对象的引用计数保持在 1,这意味着这个对象永远不能被释放并导致内存泄漏。

另一种情况更可怕——如果在两个线程中添加两个新引用只会将引用计数的值增加 1 怎么办?在某些时候,当一个引用被删除时,引用计数下降到零并且内存被收集但引用仍然存在。这将导致类似于核心转储或从内存中检索垃圾值的场景。

GIL 通过为Python添加一个全局锁来避免这种情况,在任何时间点,获取 GIL 的线程是唯一可以执行内存 IO、字节码转换和所有其他低级事情的线程。这实质上意味着虽然可能有 16 个线程,但只有获得 GIL 的线程在做工作,而所有其他线程都忙于尝试获得它。这使得Python奇怪地单线程,因为一次只运行一个线程。

更糟糕的是,没有有效的方法来移除 GIL 并保持单线程工作负载的速度。尝试使用原子递增和递减来删除 GIL 时,解释器速度减慢了 30%,这对于像 CPython 这样的语言来说是一个很大的禁忌。

好故事,但这一切与 Rust 有什么关系?

Rust 是 Mozilla Research 为安全并发而构建的语言。带有竞争条件的 Rust 代码几乎不可能编译。 Rust 编译器不会接受任何非内存或线程安全的代码。它会检查代码中是否出现任何竞争条件并且无法编译是否存在这种情况。

太好了,那为什么不能将其添加到其他编译器中以完全避免这些情况呢?

情况很复杂。 Rust 不遵循传统的编程模式。相反,它遵循所有权和借贷的过程。这意味着在任何时间点,Rust 都会确保只有一个对相关对象的可变引用。你可以有多个只读引用,但如果你想写入一个位置,你必须取得对象的所有权,然后执行突变。

Rust 的模型不能有效地直接移植到其他编译器,因为编写 Rust 代码的方式与编写 C 和 C++ 代码的方式根本不同。 Rust 真正闪耀的地方在于它在单个代码库中将安全性和性能结合在一起的方式。这就是微软在 Rust 上押下大赌注的原因,它使用它来开发开源库和项目,以解决削弱其一些核心产品的内存问题。

如果你是一名 Web 开发人员,Rust 是一种编写 Web 汇编代码的好语言。 Web Assembly 是浏览器的中级低级语言,Rust 是可以编译为 WASM 的语言之一。它非常高效,以至于 NPM 现在在他们的工具链中使用了 Rust。

Rust 将继续存在并破坏我们编写并发程序的方式,使其远离垃圾收集的世界。不断发展的社区清楚地证明了它的实力,大型科技公司对其的采用清楚地表明它是一种值得一看的语言