更好的并发案例是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::Mutex
,std::sync::RwLock
等)拥有它的保护数据。除了不使用锁然后仅在文档中处理与该锁关联的某些共享内存外,在不持有该锁的情况下也无法访问共享数据。RAII保护程序保持锁定并同时提供对锁定数据的访问权限(这可以由C ++实现,但不能由std::
锁定实现)。生命周期系统可确保您在释放锁定(放下RAII保护器)后无法继续访问数据。
当然,您可以拥有一个不包含有用数据的锁(Mutex<()>
),而只共享一些内存,而无需将其与该锁显式关联。但是,拥有可能不同步的共享内存需要unsafe
。
区别:防止意外共享
尽管可以共享内存,但是只有在明确要求时才共享。例如,当您使用消息传递(例如来自的通道std::sync
)时,生存期系统会确保在将数据发送到另一个线程之后,您不会保留对数据的任何引用。要在锁后面共享数据,您可以显式构造锁并将其提供给另一个线程。要与unsafe
您共享未同步的内存,必须使用unsafe
。
这关系到下一点:
区别:线程安全跟踪
Rust的类型系统跟踪了线程安全性的一些概念。具体而言,Sync
特征表示可以由多个线程共享的类型,而不会造成数据争用的风险,而Send
标记则表示可以从一个线程移动到另一个线程的类型。这是由编译器在整个程序中强制执行的,因此,库设计人员敢于进行优化,而如果没有这些静态检查,这将是非常危险的。例如,C ++ std::shared_ptr
始终使用原子操作来操纵其引用计数,以避免shared_ptr
在多个线程碰巧使用UB的情况下避免使用UB 。Rust具有Rc
和Arc
,不同之处仅在于Rc
使用非原子引用计数操作,并且不是线程安全的(即未实现Sync
或Send
),而Rust Arc
非常类似于shared_ptr
(并同时实现这两个特征)。
注意,如果一个类型不使用unsafe
手动实现同步,性状的存在或不存在被正确地推断。
区别:非常严格的规则
如果编译器不能完全确定某些代码没有数据争用和其他UB,则它将无法编译,period。前面提到的规则和其他工具可以使您受益匪浅,但是迟早您会希望做一些正确的事情,但是出于微不足道的原因而使编译器无法注意到。它可能是一个棘手的无锁数据结构,但也可能像“我写一个共享数组中的随机位置,但计算索引使得每个位置仅由一个线程写入”一样平凡。
那时,您可以硬着头皮添加一些不必要的同步,或者重新编写代码以使编译器可以看到其正确性(通常是可行的,有时很难,有时甚至是不可能的),或者您可以进入unsafe
代码。尽管如此,这仍然是额外的精神开销,Rust无法为您提供unsafe
代码正确性的任何保证。
差异:更少的工具
由于上述差异,在Rust中,很少有人编写可能具有数据争用的代码(或在释放后使用,或在两次释放后使用,或...)。虽然这很好,但它具有不幸的副作用,即鉴于社区的年轻人和小规模,用于追踪此类错误的生态系统的发展甚至比人们期望的要落后。
虽然原则上可以将诸如valgrind和LLVM的线程清理器之类的工具应用于Rust代码,但它是否真的有效,但因工具而异(甚至那些有效的工具也可能难以设置,尤其是因为您可能找不到最新的工具)。 -有关如何操作的最新资源)。Rust目前缺乏真正的规范,尤其是没有正式的内存模型,这并没有真正的帮助。
简而言之,尽管两种语言在功能和风险上都差不多,但是unsafe
正确编写Rust代码要比正确编写C ++代码难。当然,必须权衡以下事实:典型的Rust程序仅包含相对较小的unsafe
代码部分,而C ++程序完全是C ++。