把 skynet 的原子操作换成了 stdatomic
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
替代。但是在用的时候,需要再转换为对应的指针类型。
Comments
Posted by: loveC | (5) July 24, 2021 05:31 PM
Posted by: Anonymous | (4) July 21, 2021 03:55 PM
Posted by: PaintDream | (3) March 22, 2021 11:01 PM
Posted by: 我土鳖我自豪 | (2) January 18, 2021 12:00 PM
Posted by: 蜗牛 | (1) January 12, 2021 02:27 PM