Rust如何与C ++的并发功能相区别?


35

问题

我试图了解Rust是否从根本上充分改善了C ++的并发功能,以便决定我是否应该花时间学习Rust。

具体来说,惯用的Rust如何改善惯用的C ++的并发功能,或以任何方式背离?

改进(或分歧)主要是句法上的,还是范式上的实质改进(分歧)?或者是别的什么?还是根本就没有改善(分歧)?


基本原理

最近,我一直在尝试自学C ++ 14的并发功能,感觉有些不对劲。感觉有些不对劲。什么感觉了吗?很难说。

当涉及到并发时,似乎编译器似乎并没有真正在帮助我编写正确的程序。感觉好像我在使用汇编程序而不是编译器。

诚然,当涉及到并发时,我完全有可能遭受一个微妙的,错误的概念。也许我还没有摆脱Bartosz Milewski在有状态编程和数据竞赛之间的紧张关系。也许我不太了解编译器中有多少合理的并发方法以及操作系统中有多少并发方法。

Answers:


56

更好的并发案例是Rust项目的主要目标之一,因此,只要我们相信该项目能够实现其目标,就可以期待改进。完全免责声明:我对Rust具有很高的评价,并对此有所投资。根据要求,我将尽量避免价值判断,而是描述差异而不是(IMHO)改进

安全和不安全的锈

“ Rust”由两种语言组成:一种试图将您与系统编程的危险区分开来,另一种则更加强大,没有任何这种愿望。

不安全Rust是一种令人讨厌的粗野语言,感觉很像C ++。它使您可以执行任意危险的事情,与硬件进行对话,手动(错误地)管理内存,让自己陷入困境等。它与C和C ++非常相似,因为程序的正确性最终在您手中以及参与其中的所有其他程序员的手。您可以使用关键字来选择这种语言unsafe,就像在C和C ++中一样,在单个位置出现一个错误就会使整个项目崩溃。

Safe Rust是“默认值”,绝大多数Rust代码都是安全的,并且如果您从不unsafe在代码中提及关键字,那么您就永远不会离开安全语言。文章的其余部分将主要与该语言相关,因为unsafe代码可能破坏安全Rust难以工作给您的所有保证。另一方面,unsafe代码不是邪恶的,社区也不会将其视作邪恶(但是,在不必要时强烈建议不要这样做)。

这是危险的,是的,但也很重要,因为它允许构建安全代码使用的抽象。好的不安全代码使用类型系统来防止其他人滥用它,因此Rust程序中不安全代码的存在不必打扰安全代码。存在以下所有差异是因为Rust的类型系统具有C ++所没有的工具,并且实现并发抽象的不安全代码有效地使用了这些工具。

无差异:共享/可变内存

尽管Rust更强调消息传递并非常严格地控制共享内存,但它并不排除共享内存并发性,并且显式支持通用抽象(锁,原子操作,条件变量,并发集合)。

而且,像C ++和功能语言不同,Rust确实喜欢传统的命令式数据结构。标准库中没有持久/不可变的链表。有,std::collections::LinkedList但是就像std::list在C ++中一样,并且出于与std::list(错误使用缓存)相同的原因而不鼓励使用。

但是,参考本节的标题(“共享/可变内存”),Rust与C ++有一个区别:Rust鼓励将内存“共享XOR可变”,即永远不要共享和可变内存。时间。可以这么说,随意改变内存的使用。与此形成对比的是C ++,其中共享的可变内存是默认选项,并且被广泛使用。

尽管共享异或变量范式对于以下差异非常重要,但它也是一个完全不同的编程范式,需要一段时间才能习惯,并且存在很多限制。有时候,人们必须选择退出这种范式,例如使用原子类型(这AtomicUsize是共享的可变内存的本质)。请注意,锁也遵守shared-xor-mutable规则,因为它排除了并发读写(当一个线程写时,没有其他线程可以读写)。

无差异:数据竞争是未定义的行为(UB)

如果您在Rust代码中触发数据争用,则就像C ++一样,游戏结束了。所有的赌注都关闭了,编译器可以随心所欲地做任何事情。

但是,很难保证安全的Rust代码没有数据争用(或与此相关的任何UB)。这既扩展到核心语言又扩展到标准库。如果您可以编写一个不使用unsafe(会包含在第三方库中,但不包括标准库的)触发UB 的Rust程序,则该程序被认为是一个错误,将得到修复(这已经发生了好几次)。这当然与C ++形成了鲜明的对比,在C ++中,用UB编写程序很简单。

区别:严格的锁定规则

与C ++,在锈病(锁std::sync::Mutexstd::sync::RwLock等)拥有它的保护数据。除了不使用锁然后仅在文档中处理与该锁关联的某些共享内存外,在不持有该锁的情况下也无法访问共享数据。RAII保护程序保持锁定并同时提供对锁定数据的访问权限(这可以由C ++实现,但不能由std::锁定实现)。生命周期系统可确保您在释放锁定(放下RAII保护器)后无法继续访问数据。

当然,您可以拥有一个不包含有用数据的锁(Mutex<()>),而只共享一些内存,而无需将其与该锁显式关联。但是,拥有可能不同步的共享内存需要unsafe

区别:防止意外共享

尽管可以共享内存,但是只有在明确要求时才共享。例如,当您使用消息传递(例如来自的通道std::sync)时,生存期系统会确保在将数据发送到另一个线程之后,您不会保留对数据的任何引用。要在锁后面共享数据,您可以显式构造锁并将其提供给另一个线程。要与unsafe您共享未同步的内存,必须使用unsafe

这关系到下一点:

区别:线程安全跟踪

Rust的类型系统跟踪了线程安全性的一些概念。具体而言,Sync特征表示可以由多个线程共享的类型,而不会造成数据争用的风险,而Send标记则表示可以从一个线程移动到另一个线程的类型。这是由编译器在整个程序中强制执行的,因此,库设计人员敢于进行优化,而如果没有这些静态检查,这将是非常危险的。例如,C ++ std::shared_ptr始终使用原子操作来操纵其引用计数,以避免shared_ptr在多个线程碰巧使用UB的情况下避免使用UB 。Rust具有RcArc,不同之处仅在于Rc 使用非原子引用计数操作,并且不是线程安全的(即未实现SyncSend),而Rust Arc非常类似于shared_ptr (并同时实现这两个特征)。

注意,如果一个类型使用unsafe手动实现同步,性状的存在或不存在被正确地推断。

区别:非常严格的规则

如果编译器不能完全确定某些代码没有数据争用和其他UB,则它将无法编译,period。前面提到的规则和其他工具可以使您受益匪浅,但是迟早您会希望做一些正确的事情,但是出于微不足道的原因而使编译器无法注意到。它可能是一个棘手的无锁数据结构,但也可能像“我写一个共享数组中的随机位置,但计算索引使得每个位置仅由一个线程写入”一样平凡。

那时,您可以硬着头皮添加一些不必要的同步,或者重新编写代码以使编译器可以看到其正确性(通常是可行的,有时很难,有时甚至是不可能的),或者您可以进入unsafe代码。尽管如此,这仍然是额外的精神开销,Rust无法为您提供unsafe代码正确性的任何保证。

差异:更少的工具

由于上述差异,在Rust中,很少有人编写可能具有数据争用的代码(或在释放后使用,或在两次释放后使用,或...)。虽然这很好,但它具有不幸的副作用,即鉴于社区的年轻人和小规模,用于追踪此类错误的生态系统的发展甚至比人们期望的要落后。

虽然原则上可以将诸如valgrind和LLVM的线程清理器之类的工具应用于Rust代码,但它是否真的有效,但因工具而异(甚至那些有效的工具也可能难以设置,尤其是因为您可能找不到最新的工具)。 -有关如何操作的最新资源)。Rust目前缺乏真正的规范,尤其是没有正式的内存模型,这并没有真正的帮助。

简而言之,尽管两种语言在功能和风险上都差不多,但是unsafe正确编写Rust代码要比正确编写C ++代码。当然,必须权衡以下事实:典型的Rust程序仅包含相对较小的unsafe代码部分,而C ++程序完全是C ++。


6
+25 Upvote开关在屏幕上哪里?我找不到!这个翔实的答案是非常赞赏的。它使我对所涵盖的要点没有明显的疑问。因此,到另外一点:如果我了解Rust的文档,Rust已经[a]集成了测试工具,并且[b]了一个名为Cargo的构建系统。在您看来,这些产品是否适合生产?另外,关于Cargo,是否让我在构建过程中添加Shell,Python和Perl脚本,LaTeX编译等,这是一件很愉快的事情?
2016年

2
@thb测试的东西是非常准的(例如,没有嘲笑),但是功能正常。尽管Cargo专注于Rust和可重复性,但它的工作原理相当不错,这可能不是涵盖从源代码到最终工件的所有步骤的最佳选择。您可以编写构建脚本,但是可能不适合您提到的所有内容。(但是,人们确实经常使用构建脚本来编译C库或查找C库的现有版本,因此当您使用的不仅仅是Rust时,Cargo不会停止工作。)

2
顺便说一句,就其价值而言,您的答案似乎是定论的。自从我喜欢C ++以来,由于C ++几乎可以满足我所有需要做的事情,因为C ++稳定且被广泛使用,所以迄今为止,我很满意将C ++用于所有可能的非轻量级目的(我从不对Java产生兴趣, 例如)。但是现在我们有了并发性,在我看来C ++ 14还在为此而苦苦挣扎。十年来,我没有自愿尝试过一种新的编程语言,但是(除非Haskell应该是一个更好的选择),我认为我必须尝试Rust。
2016年

Note that if a type doesn't use unsafe to manually implement synchronization, the presence or absence of the traits are inferred correctly.实际上,即使使用unsafe元素也是如此。只是原始指针不是,Sync也不Share意味着在默认情况下包含它们的结构都不会。
Hauleth '16

@ŁukaszNiemier它可以发生在制定出好的,但也有十亿方式,其中使用不安全型可卷起SendSync即使它真的不应该。

-2

Rust也很像Erlang和Go。它使用具有缓冲区和条件等待的通道进行通信。就像Go一样,它允许您执行共享内存,支持原子引用计数和锁,并允许您在线程之间传递通道,从而放松了Erlang的限制。

但是,Rust前进了一步。当Go相信您会做正确的事时,Rust会指派一位导师与您坐在一起,并抱怨您尝试做错了什么。Rust的导师是编译器。它进行复杂的分析,以确定线程周围传递的值的所有权,并在存在潜在问题时提供编译错误。

以下是RUST文档的报价。

所有权规则在消息发送中起着至关重要的作用,因为它们有助于我们编写安全的并发代码。通过在整个Rust程序中必须考虑所有权的权衡取舍,可以防止在并发编程中出错。—带有值所有权的消息传递。

如果Erlang是严格状态,而Go是自由状态,则Rust是一个保姆状态。

您可以从编程语言的并发意识形态中找到更多信息:Java,C#,C,C +,Go和Rust


2
欢迎使用Stack Exchange!请注意,每当您链接到自己的博客时,都需要明确声明;请参阅帮助中心
Glorfindel
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.