翻译|审稿人陆新旺|如果赵云把Rust比作C++的小弟,相信大家都没有异议。Rust从C++中借鉴了很多设计思想。并发特性也是如此。Rust标准库的并发特性与C++11中的并发特性非常相似:线程、原子操作、锁和互斥锁、条件变量等等。不过这几年,随着C++17和C++20的发布,C++获得了相当多的并发相关的新特性,未来的版本会有更多值得借鉴的地方。让我们花点时间回顾一下C++的并发特性,讨论这些特性在Rust下会是什么样子,以及需要做什么才能达到这种效果。atomic_refP0019R8将std::atomic_ref引入C++。它是一种允许您将非原子对象用作原子对象的类型。例如,您可以创建一个引用常规int类型变量的atomic_ref,然后您可以使用与原子类型atomic相同的功能,就像它是atomic一样。在C++中,这需要一个全新的类型来复制大部分原子接口,而等效的Rust特性是一个单行函数:atomic*::from_mut。例如,此函数允许您将&mutu32转换为&AtomicU32,这是一种在Rust中完全正确的别名形式。C++atomic_ref类型带有需要手动维护的安全要求。只要使用atomic_ref访问一个对象,所有对该对象的访问都必须通过atomic_ref。在atomic_ref仍然存在时直接访问它会导致未定义的行为。然而,在Rust中,这完全由借用检查器处理。编译器理解通过可变地借用u32,在借用结束之前不允许任何东西直接访问该u32。进入from_mut函数的&mutu32的生命周期将作为您从它获得的&AtomicU32的一部分保留下来。您可以根据需要制作任意数量的&AtomicU32副本,但直到该引用的所有副本都消失后,原始借用才会结束。from_mut函数目前不稳定,但也许是时候稳定它了。通用原子类型在C++中,std::atomic是通用的:您可以有一个atomic,也可以有一个atomic。另一方面,在Rust中,我们只有特定的原子类型:AtomicU32、AtomicBool、AtomicUsize等。C++的原子类型支持任何大小的对象,无论平台支持如何。对于平台本机原子操作不支持的大小对象,它会自动回退到基于锁的实现。Rust只提供平台原生支持的类型。如果您正在为没有64位原子的平台进行编译,则AtomicU64不存在。这有优点也有缺点。这意味着使用AtomicU64的Rust代码可能无法在某些平台上编译,但也意味着当某些类型默默地回退到一个非常不同的实现时,不会出现与性能相关的意外。这也意味着我们可以假设AtomicU64与内存中的u64完全相同,从而允许使用像AtomicU64::from_mut这样的函数。在Rust中对任何大小的类型使用通用原子类型atomic可能很棘手。如果没有专门化,我们就不能使自动包含互斥锁而不将其包含在自动中。然而,我们可以做的是将互斥量存储在一个全局HashMap中,并通过内存地址进行索引。然后,auto可以与T大小相同,并在必要时使用来自这个全局HashMap的互斥体。这就是流行的atomic所做的。将这种泛型泛型auto类型添加到Rust标准库的提议需要讨论是否应该在no_std程序中使用它。常规哈希映射需要分配,这在no_std程序中是不可能的。固定大小的表可能适用于no_std程序,但可能出于各种原因不受欢迎。Compare-exchangewithpaddingP0528R3改变了compare_exchange处理填充的方式。atomic上的比较交换操作也用于比较填充位,但事实证明这是个坏主意。今天,填充位不再包含在比较中。由于Rust目前只为整数提供原子类型,没有任何填充,因此此更改与Rust无关。但是,使用compare_exchange方法的atomic方案需要讨论如何处理填充,并且可能需要从该方案获取输入。比较交换内存排序在C++11中,compare_exchange函数要求成功的内存顺序至少与失败顺序一样强。不接受compare_exchange(...,...,memory_order_release,memory_order_acquire)。这个要求被逐字复制到Rust的compare_exchange函数中。P0418R2认为这个限制应该作为C++17的一部分被删除。作为Rust1.64和Rustlang/Rust#98383的一部分解除了相同的限制。ConstexprmutexconstructorC++的std::mutex有一个constexpr构造函数,这意味着它可以在编译时作为常量计算的一部分进行构造。然而,并不是所有的实现实际上都提供了这个。例如,Microsoft的std::mutex实现不包含constexpr构造函数。因此,依赖于此对于可移植代码来说不是一个好主意。另外,有趣的是,C++的std::condition_variable和std::shared_mutex根本不提供constexpr构造函数。在Rust1.0中,Rust的原始互斥量不包括常量fnnew。结合Rust对静态初始化的严格要求,这使得在静态变量中使用互斥体非常烦人。作为Rustlang/Rust#93740的一部分,这在Rust1.63.0中得到修复,所有:Mutex::newrBlock::newCondvar::new现在都是常量函数。闩锁和屏障P1135R6在C++20中引入了std::ltatch和std::barriers,这两种类型都允许等待多个线程到达某个点。闩锁基本上只是一个计数器,每个线程递减它并允许您等待它达到零。它只能使用一次。屏障是这个想法的更高级版本,可以重复使用并接受一个“完成函数”,当计数器达到零时自动执行。Rust从1.0开始就有类似的屏障类型。它的灵感来自pthread(pthrea_Barrier_t)而不是C++。Rust(和pthread)的屏障不如当前包含在C++中的屏障灵活。它只有一个“自减等待”操作(称为wait),没有C++的std::barrier自带的“只等待”、“自减”、“自减删除”功能。另一方面,与C++不同,Rust(和pthread)的“递减和等待”操作将线程指定为组长。这是完成功能的(可能更灵活)替代方法。Rust版本缺少的操作可以随时轻松添加。我们所需要的只是对这些新方法的名称提出一个好的建议。信号量同样,P1135R6也向C++20添加了信号量:std::counting_semaphorestd::binary_semaphoreRust没有通用的信号量类型,尽管它确实通过thread::park和unpark信号为每个线程提供了一个高效的二进制文件。使用Mutex和Condvar可以轻松地手动构建信号量,但大多数操作系统允许使用单个AtomicU32实现更高效、更小的实现。例如,在Linux上通过futex(),在Windows上通过waitoAddress()。可用于这些操作的原子大小取决于操作系统及其版本。C++的counting_semaphore是一个模板,它以一个整数作为参数来表示我们希望能够计数到多远。例如,counting_semaphore<1000>可以计数到至少1000,因此将是16位或更大。binary_semaphore类型只是counting_Semaphore<1>的别名,在某些平台上可以是单个字节。在Rust中,我们可能不会很快为此类泛型类型做好准备。Rust的泛型强制执行一定的一致性,这对我们使用常量作为泛型参数可以做的事情施加了一些限制。我们可以有单独的信号量32、信号量64等,但这似乎有点矫枉过正。可以有一个信号量和一个信号量甚至一个信号量,但这是我们以前在标准库中没有做过的事情。我们的原子类型只是AtomicU32、AtomicU64等等。如上所述,对于我们的原子类型,我们仅提供您正在编译的平台本机支持的类型。如果我们将相同的理念应用于信号量,它就不会存在于没有futex或WaitoAddress功能的平台上,例如macOS。如果我们有不同大小的单独信号量类型,某些大小在Linux和各种BSD上不存在(某些版本)。如果我们想在Rust中使用标准的信号量类型,我们首先需要一些输入,比如我们是否真的需要不同大小的信号量,以及需要什么样的灵活性和可移植性才能使它们有用。也许我们应该只使用一种始终可用的32位信号量类型(带有基于锁的回退),但任何此类建议都必须包括对用例和限制的详细说明。原子等待和通知P1135R6添加到C++20的其余新功能是原子等待和通知功能。这些函数通过标准接口有效地直接公开了Linux的futex()和Windows的waitoAddress()。但是,无论操作系统支持什么,它们都适用于所有平台的所有大小的原子。LinuxFutex(在FUTEX2之前)始终是32位的,但C++也允许atomic:wait。一种方法是使用类似于“停车场”的东西:一个有效地将内存地址映射到锁和队列的全局哈希映射。这意味着Linux上的32位等待操作可以使用非常快速的基于futex的实现,而其他规模的操作将使用非常不同的实现。如果我们遵循只提供本机支持的类型和函数的理念(就像我们对原子类型所做的那样),我们就不会提供这样的后备实现。这意味着我们在Linux上只有AtomicU32::wait(和AtomicI32::wait),而在Windows上,所有原子类型都包含此等待方法。在Rust中使用Atomic*::wait和Atomic*::notify需要讨论回退到全局表在Rust中是否合适。jthread和stop_tokenP0660R10将std::jthread和std::stop_token添加到C++20。如果我们暂时忽略stop_token,jthread基本上只是一个常规的std::thread,它在销毁时自动获得一个join()方法。这避免了意外分离线程并使其运行时间超过预期,这在常规线程中可能会发生。然而,它也引入了一个潜在的新陷阱:立即销毁jThread对象将立即加入线程,从而有效地消除任何潜在的并行性。从Rust1.63.0开始,提供了作用域线程(Rustlang/Rust#93203)。与jthread一样,作用域线程会自动加入。但是,它们的连接点定义明确,保证安全可靠。借用检查器甚至理解这种保证,允许您在作用域线程中安全地借用局部变量,只要这些变量超出作用域即可。jthreads的一大特点,除了自动加入外,就是它的stop_token和对应的stop_source。您可以在stop_source上调用request_stop()使stop_token上相应的stopUrequest()方法返回true。这可以很好地要求线程停止,并且在加入之前在jthread的析构函数中自动完成。由线程的代码实际检查令牌,并在设置时停止。到目前为止,它看起来几乎像一个普通的AtomicBool。不同之处在于stop_callback类型。此类型允许使用停止令牌注册回调函数,即“停止函数”。使用相应的停止源请求停止将执行此功能。事实上,线程可以使用它来让其他线程知道如何停止或取消它们的工作。在Rust中,我们可以轻松地向thread::Scope的Scope对象添加类似布尔值的原子功能。一个简单的is_finished(&self)->bool或stop_requested(&self)->bool指示主范围函数是否已完成可能就足够了。可以结合request_stop(&self)方法从任何地方请求它。stop_callback功能更复杂,任何Rust等同物可能需要详细的提案来讨论其接口、用例和限制。原子浮点P0020R6在C++20中添加了对原子浮点加法和减法的支持。在Rust中添加AtomicF32或AtomicF64也很容易,但矛盾的是,目前原生支持原子浮点运算的平台似乎往往是GPU厂商,而Rust现在似乎并不提供对这些平台的支持。强烈推荐将这些类型添加到Rust的一些实际用例。字节原子内存目前,不可能在Rust或C++中有效地实现遵循内存模型所有规则的顺序锁。P1478R7建议在未来的C++版本中添加atomic_load_per_byte_memcpy和atomic_store_per_byte_memcpy来解决这个问题。对于Rust,这里有一个通过AtomicPerByte类型公开功能的想法:RFC3301。atomicshared_ptrP0718R2将atomic和atomic的特化添加到C++20。引用计数指针(C++中的shared_ptr,Rust中的Arc)通常用于并发无锁数据结构。原子特化使得通过正确处理引用计数更容易正确地做到这一点。在Rust中,我们可以添加等效的AtomicArc和AtomicWeak类型。(虽然AtomicArc听起来有点奇怪,但考虑到Arc的A已经代表“原子”。)但是,C++的shared_ptr是可以为null的,而在Rust中,它需要一个option。不清楚AtomicArc是否应该为空,或者我们是否也应该有一个AtomicOptionArc。流行的arc-swap已经在Rust中提供了所有这些变体,但据我所知,还没有任何类似标准库的提议。synchronized_value虽然P0290R2没有被接受,但一个名为synchronized_value的类型被提出,它结合了互斥锁和数据类型T。尽管它当时没有被C++接受,但这是一个有趣的建议,因为synchronized_value是几乎与Rust中的Mutex相同。在C++中,std::mutex不包含它保护的数据,甚至不知道它保护的是什么。这意味着由用户记住哪些数据受保护以及由哪个互斥锁保护,并确保每次访问“受保护”数据时锁定正确的互斥锁。Rust的Mutex设计,使用类似于(可变)T引用的MutexGuard,这在提高安全性的同时仍然允许Mutex<()>。synchronized_value提案试图将此模式添加到C++,但使用闭包而不是互斥锁,因为C++不跟踪生命周期。结论在作者看来,C++可以继续成为Rust的灵感来源。虽然“直接复制粘贴”的想法不值得提倡,但是好的想法还是需要学习和继承的。正如我们在Mutex、作用域线程、Atomic*::from_mut等中看到的那样,在Rust中提供相同的功能时,事情往往非常不同。当然,提供与C++完全相同的功能不应该是主要目标。目标应该是准确地提供Rust生态系统从语言和标准库中需要的东西,这可能与C++用户从他们的语言中需要的东西不同。如果您对Rust标准库的并发性需求当前未得到满足,请随时将其留在评论中,无论它是否已经用另一种语言解决。原文链接:https://blog.m-ou.se/rust-cpp-concurrency/译者介绍卢信旺,社区编辑,编程语言爱好者,对数据库、架构、云原生有浓厚兴趣,目前在职为一家跨境电商海外营销公司,负责后端开发。