Skynet专题之:原子操作

JavenLaw

原子操作 和 锁 都用于处理多线程环境下的并发访问问题,因此下面将统一解释它们


原子操作&锁的区别

在并发编程中

原子性是指一个操作要么完全执行,要么完全不执行,没有中间状态

它是在底层硬件或操作系统级别执行的操作,是不可中断的单个操作,具有原子性和互斥性

原子性操作的目的是:保证在多线程环境下对共享数据的操作不会出现竞态条件

锁是一种同步机制,用于实现临界区的互斥访问

它保证同一时间只有一个线程可以获得锁并执行临界区代码,避免多个线程同时修改共享数据而导致的数据竞争问题

锁的作用是提供互斥性,但锁并不保证临界区内的操作是原子的

锁的原子性体现在:

​ 当一个线程获得了锁并进入临界区时,它可以确保在持有锁的期间不会被其他线程打断

​ 这确实具有一种原子性,即在给定的上下文中,临界区的执行是不可中断的

锁的目的是:保护共享资源,以确保在任何给定时间只有一个线程可以访问临界区

第一,各自的作用

原子:

  • 原子操作用于对共享数据进行原子性的读取和修改,确保多个线程对同一数据进行操作时不会引发竞态条件和数据不一致的问题
  • 原子操作可以保证某个特定操作在执行期间不会被其他线程中断,从而确保操作的完整性和一致性

锁:

  • 锁用于实现临界区的互斥访问,确保同一时间只有一个线程可以进入临界区执行操作,从而避免多个线程同时修改共享数据而导致的数据竞争问题
  • 锁可以保证在一个线程执行临界区代码时,其他线程会被阻塞,等待当前线程释放锁后才能继续执行

第二,区别和好处

粒度:

  • 原子操作通常用于对单个变量或对象的操作,提供了更细粒度的并发控制。它可以在不阻塞其他线程的情况下,对共享数据进行原子性的读取和修改
  • 锁通常用于对一段代码(临界区)的访问控制,提供了更粗粒度的并发控制。它可以确保同一时间只有一个线程可以执行临界区代码

开销:

  • 原子操作通常比锁的开销更小,因为它不需要上下文切换和线程阻塞等额外开销。原子操作使用硬件级别的原子指令来实现,执行速度较快
  • 锁的实现可能涉及线程调度和上下文切换,需要更多的开销。当临界区的代码较长或复杂时,锁的开销可能会更高

场景:

  • 原子操作适用于对共享数据进行简单的读取和修改操作,如计数器、标志位等。它们可以在不阻塞其他线程的情况下,保证数据的一致性
  • 锁适用于需要对一段代码进行互斥访问的情况,如修改共享数据的复杂算法、数据结构的更新等。它们可以确保在同一时间只有一个线程执行临界区代码,避免数据竞争和不一致性


原子操作的实现

一般是如何使用原子操作的呢?几乎都是使用封装好的原子操作函数和库

主要分为3类:

​ C/C++标准:atomic 头文件 + stdatomic.h 头文件

​ GCC实现:GCC 编译器提供了一些内建函数

​ POSIX标准:pthread.h 头文件

atomic 库,是 C++ 标准库中的一部分,属于 C++ 标准

atomic 是 C++11 原子操作的一部分,提供了原子操作的支持,包含在 atomic.h 头文件中。它提供了一组模板类和函数,用于对各种类型的数据进行原子操作,比如加载、存储、交换、逻辑操作等。

atomic 主要用于 C++ 中

常用的有:

​ std::atomic:模板类型,提供原子操作支持的变量类型,如 std::atomic

​ std::atomic_load:从原子变量中加载值

​ std::atomic_store:将值存储到原子变量中

​ std::atomic_exchange:交换原子变量的值

​ std::atomic_compare_exchange_weak 和 std::atomic_compare_exchange_strong:比较并交换原子变量的值。若相等,则交换

特点是:std::atomic_开头

stdatomic.h 头文件,是 C11 标准库中引入的,属于 C 标准

stdatomic.h 是 C11 引入的原子操作的一部分,包含了一组宏,提供了原子操作的支持。C11 标准库中的原子操作使用 stdatomic.h 头文件。这些宏定义了一些原子类型(比如 atomic_int、atomic_flag 等)以及一些原子操作函数(比如 atomic_load、atomic_store、atomic_exchange等)。

stdatomic.h 主要用于 C 中

常用的有:

​ atomic_load:从原子变量中加载值

​ atomic_store:将值存储到原子变量中

​ atomic_exchange:交换原子变量的值

​ atomic_compare_exchange_weak 和 atomic_compare_exchange_strong:比较并交换原子变量的值。若相等,则交换

​ atomic_flag:原子标志类型,用于简单的互斥锁

特点是:atomic_ 开头

GCC 内建函数

GCC 编译器提供了一些内建函数,用于实现原子操作,这些函数可以在 C 和 C++ 中使用

常用的有:

​ __ sync_bool_compare_and_swap:进行原子的比较和交换操作

​ __ sync_fetch_and_add:原子的加法操作

特点是:以 __sync 为开头

pthread.h

pthread.h 是 POSIX 线程库的头文件,用于多线程编程。它提供了创建、管理和同步线程的函数和类型定义。

虽然 pthread.h中也包含了一些原子操作相关的函数,比如 pthread_mutex_lock 和 pthread_mutex_unlock 用于实现互斥锁

但它并不是专门用于原子操作的库

要在 C 或 C++ 中进行原子操作,更推荐使用 atomic.h(C++)或 stdatomic.h(C)中提供的原子操作函数和类型

常用的有:

​ pthread_mutex_t

​ pthread_mutex_init

​ pthread_mutex_lock

特点是:以 pthread_ 开头


改进历史

云风的 BLOG: 把 skynet 的原子操作换成了 stdatomic (codingnow.com)


Skynet种的atomic.h

再来看看skynet如何调用,封装这些原子操作的

// 特别说明:网页显示问题,以下代码的 预处理语句 都省略了 #
// 文件位于:atomic.h
ifndef SKYNET_ATOMIC_H // 如果没有定义SKYNET_ATOMIC_H,则开始定义SKYNET_ATOMIC_H
		define SKYNET_ATOMIC_H
		include <stddef.h>
		include <stdint.h>

		// __STDC_NO_ATOMICS__ 是一个预定义宏,用于指示编译器是否支持 C11 中的 <stdatomic.h> 头文件中定义的原子操作
		// 如果__STDC_NO_ATOMICS__ 没有被定义,编译器支持 C11 的原子操作,
		// 如果__STDC_NO_ATOMICS__ 被定义,则表示编译器不支持 C11 的原子操作
		// 因此,可以使用 #ifdef __STDC_NO_ATOMICS__ 来判断编译器是否支持 C11 中的原子操作
		// 如果支持,则可以使用 <stdatomic.h> 中定义的原子操作,否则可能需要使用其他方式来实现原子操作
		ifdef __STDC_NO_ATOMICS__ 
				// 这里表示编译器不支持 C11 的原子操作,因此需要下面的方式来实现原子操作
				// 即:使用GCC编译器内置的函数来代替POSIX标准中定义的原子操作函数

				// 使用 volatile 关键字可以告诉编译器不要对变量进行优化
				// 以确保每次访问变量时都从内存中读取最新的值,而不是使用之前缓存的值
				// 这对于需要与并发操作或外部事件进行交互的代码非常重要,以确保数据的一致性和可靠性
				
				define ATOM_INT volatile int // 表示 int 类型的变量
				define ATOM_POINTER volatile uintptr_t // 表示 uintptr_t 类型的变量
				define ATOM_SIZET volatile size_t // 表示 size_t 类型的变量
				define ATOM_ULONG volatile unsigned long // 表示 unsigned long 类型的变量
				define ATOM_INIT(ptr, v) (*(ptr) = v) // 初始化原子类型的对象
				define ATOM_LOAD(ptr) (*(ptr)) // 从原子变量中加载值
				define ATOM_STORE(ptr, v) (*(ptr) = v) // 将值存储到原子变量中
				
				// GCC 内置函数,实现原子的比较和交换操作,适用于对 int 类型的变量 进行原子的比较和交换操作
				define ATOM_CAS(ptr, oval, nval) __sync_bool_compare_and_swap(ptr, oval, nval)
				
				// GCC 内置函数,实现原子的比较和交换操作,适用于对 unsigned long 类型的变量 进行原子的比较和交换操作
				define ATOM_CAS_ULONG(ptr, oval, nval) __sync_bool_compare_and_swap(ptr, oval, nval)
				
				// GCC 内置函数,实现原子的比较和交换操作,适用于对 size 类型的变量 进行原子的比较和交换操作
				define ATOM_CAS_SIZET(ptr, oval, nval) __sync_bool_compare_and_swap(ptr, oval, nval)
				
				// GCC 内置函数,实现原子的比较和交换操作,适用于对 指向指针的指针 进行原子的比较和交换操作
				define ATOM_CAS_POINTER(ptr, oval, nval) __sync_bool_compare_and_swap(ptr, oval, nval)

				// GCC 内置函数,原子的加法操作,实现 原子操作+1
				define ATOM_FINC(ptr) __sync_fetch_and_add(ptr, 1)
				
				// GCC 内置函数,原子的减法操作,实现 原子操作-1
				define ATOM_FDEC(ptr) __sync_fetch_and_sub(ptr, 1)
				
				// GCC 内置函数,原子的加法操作,实现 原子操作+n
				define ATOM_FADD(ptr,n) __sync_fetch_and_add(ptr, n)

				// GCC 内置函数,原子的减法操作,实现 原子操作-n
				define ATOM_FSUB(ptr,n) __sync_fetch_and_sub(ptr, n)

				// GCC 内置函数,原子的与操作,实现 原子操作 与运算
				define ATOM_FAND(ptr,n) __sync_fetch_and_and(ptr, n)

		else
				// 编译器支持 C11 中的原子操作,不需要用GCC编译器内置的函数来代替POSIX标准中定义的原子操作函数
				// 即:使用stdatomic.h文件即可
				
				// 在开始之前,还需要判断编译器是否支持C++
				// 如果支持C++,就用 atomic.h文件 来实现原子操作函数
				// 如果不支持C++,就用 stdatomic.h文件 来实现原子操作函数
				
				// 如果编译器是 C++ 编译器,那么会包含 <atomic> 头文件,并定义 STD_ 为 std::,用于在代码中指明标准命名空间std
				// 
				// 如果编译器不是 C++ 编译器,那么会包含 <stdatomic.h> 头文件,并将 STD_ 和 atomic_value_type_ 定义为空
				// 因为在 C 中不需要指明命名空间,并且 stdatomic.h 中已经提供了相应的宏定义

				// 这样做的目的是为了使代码在 C 和 C++ 中都能正常编译和工作,而不需要修改原子操作的代码
				
				// __cplusplus 是一个预定义宏,用于指示编译器是否正在编译 C++ 代码
				if defined (__cplusplus)
					include <atomic>
					define STD_ std::
					define atomic_value_type_(p, v) decltype((p)->load())(v) 
				else
					include <stdatomic.h>
					define STD_
					define atomic_value_type_(p, v) v
				endif
				// 特别关于 atomic_value_type_ 的定义
				// 
				// decltype 是 C++ 中的一个关键字,用于获取表达式的类型而不执行实际的表达式
				// 例如:decltype(expression)
				// 		expression是一个表达式,可以是变量、函数调用、类型转换等
				// 
				// decltype 的作用是根据表达式的类型推导出一个类型,并在编译时确定该类型
				// 
				// 1,推导变量的类型:可以使用 decltype 来声明一个变量,该变量的类型与给定表达式的类型相同
				// 					int x = 5;
				// 					decltype(x) y;   	// y 的类型与给定表达式的类型相同,都为int
				//
				// 2,推导函数返回值的类型:可以使用 decltype 来推导函数的返回值类型,尤其在函数返回类型比较复杂或依赖于参数类型等情况下
				// 					int foo();
				// 					decltype(foo()) result;  // result 的类型与 foo() 的返回类型相同
				//
				// 那么 (p)->load() 又是什么含义,有什么作用呢?
				// 在 C++ 中,p 是一个指向 std::atomic<int> 类型对象的指针,而 p->load() 是调用了 std::atomic 类的 load() 成员函数
				// (后面我们能知道,我们通过 atomic_init(ref, v) 初始化 atomic 原子对象,所以传进来的 p 也一定是 atomic 对象)
				// std::atomic 是 C++ 中用于实现原子操作的模板类,load() 是它的一个成员函数,用于原子地读取 std::atomic 对象的值
				// 在这种情况下,p->load() 表示通过指针 p 原子地访问到的 std::atomic<int> 对象的值
				// 
				// 现在我们知道 #define atomic_value_type_(p, v) decltype((p)->load())(v) 的作用了
				// atomic_value_type_(p, v) 就是把 传入的 v 的类型,转为 (p)->load() 的返回值的类型
				
				// C11 标准中 <stdatomic.h> 头文件中定义的一种原子类型
				// atomic_int用于表示 int 类型的变量
				define ATOM_INT  STD_ atomic_int
				
				// C11 标准中 <stdatomic.h> 头文件中定义的一种原子类型
				// atomic_uintptr_t用于表示 uintptr_t 类型的变量
				define ATOM_POINTER STD_ atomic_uintptr_t

				// C11 标准中 <stdatomic.h> 头文件中定义的一种原子类型
				// atomic_size_t用于表示 size_t 类型的变量
				define ATOM_SIZET STD_ atomic_size_t

				// C11 标准中 <stdatomic.h> 头文件中定义的一种原子类型
				// atomic_ulong 是用于表示 unsigned long 类型的变量
				define ATOM_ULONG STD_ atomic_ulong 
				
				define ATOM_INIT(ref, v) STD_ atomic_init(ref, v) // 用于初始化原子类型的对象
				define ATOM_LOAD(ptr) STD_ atomic_load(ptr) // 从原子变量中加载值
				define ATOM_STORE(ptr, v) STD_ atomic_store(ptr, v) // 将值存储到原子变量中

				
				// C++ 中的原子操作函数,用于比较并交换原子类型的值,适用于对 int 类型的变量
				static inline int
				ATOM_CAS(STD_ atomic_int *ptr, int oval, int nval) {
					return STD_ atomic_compare_exchange_weak(ptr, &(oval), nval);
				}

				// C++ 中的原子操作函数,用于比较并交换原子类型的值,适用于对 size_t 类型的变量
				static inline int
				ATOM_CAS_SIZET(STD_ atomic_size_t *ptr, size_t oval, size_t nval) {
					return STD_ atomic_compare_exchange_weak(ptr, &(oval), nval);
				}
				
				// C++ 中的原子操作函数,用于比较并交换原子类型的值,适用于对 unsigned long 类型的变量
				static inline int
				ATOM_CAS_ULONG(STD_ atomic_ulong *ptr, unsigned long oval, unsigned long nval) {
					return STD_ atomic_compare_exchange_weak(ptr, &(oval), nval);
				}

				// C++ 中的原子操作函数,用于比较并交换原子类型的值,适用于对 uintptr_t 类型的变量
				static inline int
				ATOM_CAS_POINTER(STD_ atomic_uintptr_t *ptr, uintptr_t oval, uintptr_t nval) {
					return STD_ atomic_compare_exchange_weak(ptr, &(oval), nval);
				}


				define ATOM_FINC(ptr) STD_ atomic_fetch_add(ptr, atomic_value_type_(ptr, 1)) // 原子操作+1
				define ATOM_FDEC(ptr) STD_ atomic_fetch_sub(ptr, atomic_value_type_(ptr, 1)) // 原子操作-1
				define ATOM_FADD(ptr,n) STD_ atomic_fetch_add(ptr, atomic_value_type_(ptr, n)) // 原子操作+n
				define ATOM_FSUB(ptr,n) STD_ atomic_fetch_sub(ptr, atomic_value_type_(ptr, n)) // 原子操作-n
				define ATOM_FAND(ptr,n) STD_ atomic_fetch_and(ptr, atomic_value_type_(ptr, n)) // 原子操作 与运算
		endif
endif

现在无论我们是使用 GCC 内置函数,还是 C/C++ 标准函数来编译我们的代码,都封装了以下的函数

类型声明类

​ int型:ATOM_INT

​ uintptr_t型:ATOM_POINTER

​ size_t型:ATOM_SIZET

​ unsigned long型:ATOM_ULONG

基础操作类

​ 初始化操作:ATOM_INIT(ref, v)

​ 加载操作:ATOM_LOAD(ptr)

​ 存储操作:ATOM_STORE(ptr, v)

实际计算类

​ 加1操作:ATOM_FINC(ptr)

​ 减1操作:ATOM_FDEC(ptr)

​ 加n操作:ATOM_FINC(ptr,n)

​ 减n操作:ATOM_FDEC(ptr,n)

​ 与操作:ATOM_FAND(ptr,n)

​ int型的比较和交换:ATOM_CAS(STD_ atomic_int *ptr,int oval,int nval)

​ size_t的比较和交换:ATOM_CAS_SIZET(STD_ atomic_size_t *ptr,size_t oval,size_t nval)

​ uintptr_t的比较和交换:ATOM_CAS_POINTER(STD_ atomic_uintptr_t *ptr,uintptr_t oval,uintptr_t nval)

​ unsigned long的比较和交换:ATOM_CAS_ULONG(STD_ atomic_ulong *ptr,unsigned long oval,unsigned long nval)


原子操作的使用

最后我们来看看具体的使用方法,以 skynet_globalinit() 的代码举例:

// 声明
struct skynet_node {
    // 参看上面的 类型声明类 - int型:ATOM_INT
    // 在 skynet_node 结构体中,声明了原子对象 total,类型为 int型
	ATOM_INT total;
    
	// 其他代码可以忽略
};

static struct skynet_node G_NODE; // 建立全局单例G_NODE

// 初始化
void skynet_globalinit(void) {
    // 参看上面的 基础操作类 - 初始化操作:ATOM_INIT(ref, v)
    // 把刚才在 skynet_node 结构体中声明的原子对象 total,初始化为0
	ATOM_INIT(&G_NODE.total, 0);
	
    // 其他代码可以忽略
}

// 读取操作
int skynet_context_total() {
	return ATOM_LOAD(&G_NODE.total); // 参看上面的 基础操作类 - 加载操作:ATOM_LOAD(ptr),实际就是读取数值
}

// 增加操作
static void context_inc() {
	ATOM_FINC(&G_NODE.total); // 参看上面的 基础操作类 - 加1操作:ATOM_FINC(ptr)
}

// 减少操作
static void context_dec() {
	ATOM_FDEC(&G_NODE.total); // 参看上面的 基础操作类 - 减1操作:ATOM_FDEC(ptr)
}

以上,我们调用Skynet封装的函数,就能很轻松地实现原子操作了

另外,自旋锁和读写锁也是根据原子操作来实现的