把 skynet 的原子操作换成了 stdatomic

云风 2021-01-12 12:13

stdatomic已经是 C11 的标准,并且成为了 C++ 标准的一部分。msvc 也将会支持 stdatomic。在 skynet 项目开始的时候,还没有这个可以用,所以我采用的是更早一点的gcc sync 系列的扩展

我想,用 stdatomic 来实现原子操作,会有利于 skynet 未来的发展。上周花了点时间熟悉这套 api 并在 skynet 中实现。同时保留了之前的实现,如果编译器定义了__STD_NO_ATOMICS__就会切换回老版本。

首先,C11 增加了_Atomic(T)类型。这在之前的 gcc Atomic builtins 里没有。过去是直接使用已有的原生数据类型的。现在,如果一个 int 是一个原子变量,就需要使用atomic_int,它实际上是_Atomic(int)。

原子操作的 api 只接受原子变量,即使是简单的读写原子变量,也必须用atomic_load和atomic_store。之前则没有提供相关 api 。这样代码更为严谨,你不必假设系统到底能原子读写多长的字,编译器会做检查。

不过,gcc 似乎并没有严格检查原子操作传入的变量是否是原子类型;而 clang 则严格的多。所以一开始我实现的初版在 clang 上遇到的编译操作,就是因为改漏了一些地方,经过网友提醒,后来才修补完整。

cas 指令,即先比较旧值,只有在相等的情况下才做交换。这是无锁结构和一些基本锁实现的基础。例如 CAS(ref, 0, 1) 表示比较 *ref 是否为 0 ,如果不等于 0 就返回失败;等于 0 就返回成功并把 *ref 置为 1 。

这可以在并发操作 *ref 的时候,同时只有一处可以把 *ref 从 0 修改为 1 。

在新标准中, cas 分成了两个版本,atomic_compare_exchange_weak和atomic_compare_exchange_strong。weak 版本允许偶发情况下,即使相等也失败(对于上面的例子来说,允许当 *ref == 0 的时候失败)。我们一般使用这个 weak 版本即可(相对 strong 版本成本可能更低)。

不过让我奇怪的是,新标准的 cas api 的 oval 旧值是用指针传递,而新值用的值传递。这个旧值指针是一个传统指针,不是原子类型。我不太理解这个设计的原因。之前的 atomic builtins 和 windows 的 InterlockedCompareExchange 都是传值的。

之前的__sync_lock_test_and_set变成了atomic_flag_test_and_set,必须使用atomic_flag。这个东西可以用来实现 spinlock 。但让我奇怪的是,C11 标准中没有atomic_flag_test(在 C++20 中有)。而实现读写锁则需要 test (但不 set),不过没太大关系,我们可以用 cas 指令代替。

在以前的 atomic builtins 中,既有__sync_fetch_and_add又有__sync_add_and_fetch,区别在于返回值是加之前的还是加之后的。stdatomic 中只保留了atomic_fetch_and_add,取消了后者。不过没太大关系,因为,以自增 1 为例,add_and_fectch(ref,1)等价于fetch_and_add(ref, 1)+1。

最后一个小问题是,指针没有定义 atomic 类型,所以我用atomic_uintptr_t替代。但是在用的时候,需要再转换为对应的指针类型。

[返回] [原文链接]