目录
原子操作
1. 原子操作简介
2. 原子操作的优点
3. RT-Thread 原子操作的实现与使用方法
4. RT-Thread 原子操作 API
原子读
原子写
原子数据交换
原子加
原子减
原子异或
原子与
原子或
原子标志检查与置位
原子标志清除
原子比较与交换
5. 综合示例
原子操作
1. 原子操作简介
原子操作(Atomic operation)是指一种不可分割的操作,要么完全执行成功,要么完全不执行。原子操作的执行过程中不允许有任何中断,如果出现了中断,那么操作的结果就无法保证。原子操作通常用于多线程编程中,保证多个线程之间的并发执行不会出现数据竞争等问题。
在实现原子操作时,通常使用硬件指令或者操作系统提供的原子操作函数来保证操作的原子性。 在应用层面,原子操作可以用于实现一些高级的同步和并发控制机制。例如,在多线程编程中,如果多个线程都需要访问同一个共享变量,为了避免数据竞争问题,可以使用原子操作来保证对该变量的操作是原子的。我们以 ARM 内核执行一个 i++操作为例:
movl i, %eax //内存访问,读取 i 变量到 cpu 的 eax 寄存器
addl $1, %eax //修改寄存器的值
movl %eax, i //将寄存器中的值写回内存
复制错误复制成功
我们看到对于编码的工程师我们执行一个 i++的操作仅需一行C语言代码,在编译后 i++就会被翻译成三条汇编指令,所以在这三条指令之间是可能会被系统调度、中断等事件打断的,因而我们在一些场景就需要一气呵成执行完上述操作,原子操作就具备这样的能力。
备注:
操作系统的线程调度的时间片,是以执行汇编指令为基本单位的,而不是以C语言语句为最基本的执行单元。一条C语言语句会编译成多条汇编指令!!!
2. 原子操作的优点
在 RT-Thread 中我们可以采取开关全局中断,调度器上锁等方式对临界区资源进行保护,其他 OS 也会提供类似的操作,若采用原子操作后我们可以提高临界区代码的执行效率,大幅提升系统的运行效率,同时也会在一定程度上降低编程的复杂度
关中断解决的中断与线程之间访问共享资源冲突问题;
互斥锁解决的是线程与线程之间访问共享变量的冲突问题。
中断和线程在访问共享资源时可能遇到的冲突,以及如何解决这些冲突。
现在,让我们更详细地讨论一下这两个概念及其解决方案。
中断与线程访问共享资源的冲突
- 中断:中断是硬件或软件向CPU发出的信号,用于请求CPU暂停当前正在执行的程序,转而执行与中断相关的处理程序。中断处理程序执行完毕后,CPU会返回到被中断的程序并继续执行。
- 冲突:当中断处理程序和线程同时访问同一共享资源时,可能会发生数据不一致或破坏的情况。
- 解决方案:关中断
- 为了避免这种情况,在操作系统或某些硬件驱动中,可能会在访问某些关键共享资源时暂时关闭中断。这样,中断处理程序就不会打断当前线程的执行,从而避免了潜在的冲突。
- 但这种方法有一个明显的缺点:它会降低系统的响应性,因为中断是系统响应外部事件(如硬件输入、定时器到期等)的重要方式。因此,关中断应该尽可能短,并且只在必要时使用。
线程与线程之间访问共享变量的冲突
- 线程:线程是操作系统调度的最小单位,是进程中的一个执行流。多个线程可以共享进程的资源,包括内存空间、文件句柄等。
- 冲突:当多个线程同时访问并修改同一个共享变量时,可能会导致数据不一致或破坏的情况。这种现象被称为“竞态条件”(Race Condition)。
- 解决方案:互斥锁(Mutex)
- 互斥锁是一种同步原语,用于保护共享资源,防止多个线程同时访问。当一个线程获得互斥锁时,其他尝试获取该锁的线程将被阻塞,直到锁被释放。
- 使用互斥锁可以确保在任何时候只有一个线程能够访问被保护的共享资源,从而避免了竞态条件。
- 但使用互斥锁也需要谨慎,因为过度或不当地使用它们可能会导致死锁(Deadlock)或其他同步问题。
总之,关中断和互斥锁是解决不同类型冲突的有效方法,但都需要谨慎使用以避免引入其他问题
下文是一个简单变量自增的示例:
采用开关全局中断的方式实现的对临界区的保护:
...
int a = 5;
level = rt_hw_interrupt_disable();
a++;
rt_hw_interrupt_enable(level);
...
复制错误复制成功
我们若采用 RT-Thread 提供的原子操作 API 可以这么做:
...
int a = 5;
rt_atomic_add(&a,1);
...
复制错误复制成功
显然采用原子操作的方式更加简单一些,且避免了开关全局中断带来的性能损失。
3. RT-Thread 原子操作的实现与使用方法
RT-Thread 对 32-bit 的 ARM、32-bit 的 RISC-V 与 64-bit 的 RISC-V 中支持原子操作的内核提供了原子操作支持,使用对应平台的原子操作汇编指令与相关指令实现,默认支持,无需用户关心实现,用户使用时仅需在工程包含rtatomic.h
即可使用该文件提供的原子操作 API,详细支持情况如下:
指令架构 | RT-Thread 适配内核的原子指令支持情况 |
---|---|
32-bit ARM | 采用 ARM 指令集的绝大多数内核支持原子指令,不支持的内核有 cortex-m0,cortex-m0+,arm926,lpc214x,lpc24xx,s3c24x0,AT91SAM7。 |
32-bit RISC-V | 采用 RV32 指令集的大部分内核支持原子操作,部分不支持的 BSP 有:core-v-mcu,rv32m1_vega。 |
64-bit RISC-V | 采用 RV64 指令集的大部分内核支持原子操作,部分不支持的 BSP 有:juicevm。 |
若工具链支持 C11 标准的原子操作 API 也可以使用 menuconfig 配置RT_USING_STDC_ATOMIC
宏,此时调用rtatomic.h
中提供的宏实际上最终会调用 C11 标准提供的 API,menuconfig 的配置方法如下:
RT-Thread Kernel --->
[*]Use atomic implemented in stdatomic.h
复制错误复制成功
对于不支持原子操作的内核,用户在工程中包含rtatomic.h
并使用该文件提供的 API 时,此时会采用开关全局中断的方式软实现原子操作。
原子操作(Atomic Operation)是指在多线程或并发环境中,不会被其他线程或进程打断的操作。这些操作在执行过程中是不可中断的,从而保证了对共享资源访问的完整性和一致性。在汇编语言中,一些指令被设计为原子操作,以确保它们在执行过程中不会被其他线程或进程干扰。
以下是一些常见的原子操作汇编指令(这些指令可能因处理器架构和指令集的不同而有所差异):
- x86 架构(如 Intel 和 AMD)
xchg
:交换两个寄存器或内存位置的值。lock prefix
:与某些指令(如add
,sub
,inc
,dec
,and
,or
,xor
,cmpxchg
,bts
,btr
,btc
,btsc
,xadd
,cmpxchg8b
,cmpxchg16b
等)结合使用,以确保操作的原子性。cmpxchg
:比较并交换,如果目标位置的值与给定值相等,则将该位置的值替换为新值。xadd
:交换并加法,将源操作数的值加到目标操作数上,并将目标操作数的原始值存储到源操作数中。cmpxchg8b
和cmpxchg16b
:用于比较并交换 64 位和 128 位的数据。- ARM 架构
LDREX
(Load Exclusive)和STREX
(Store Exclusive):这两个指令一起使用来实现原子操作。LDREX
加载一个值并标记内存位置为“独占”,然后STREX
尝试存储一个新值到该位置。如果自LDREX
以来该位置没有被其他处理器修改过,则STREX
成功并返回 0;否则返回非 0 值。LDREXD
和STREXD
:这些是 64 位版本的LDREX
和STREX
。LDAEX
(Load-Acquire Exclusive)和STLEX
(Store-Release Exclusive):这些是 ARMv8 架构中引入的指令,用于支持更复杂的内存顺序模型。- 其他架构
- 其他处理器架构(如 MIPS、PowerPC、RISC-V 等)也有自己的原子操作指令集,但具体指令和用法各不相同。
请注意,在使用原子操作时,需要确保你的代码符合目标平台的内存模型和并发模型。此外,编译器也可能提供内置函数或内联汇编来简化原子操作的使用。例如,在 C/C++ 中,可以使用
<stdatomic.h>
头文件中的函数来进行原子操作。最后,虽然原子操作在某些情况下很有用,但它们并不是解决所有并发问题的万能药。过度使用原子操作可能会导致性能下降和复杂性增加。在设计并发系统时,应该仔细考虑并发控制策略,并根据具体需求选择适当的同步原语。
原子操作指令虽然为并发编程提供了强大的工具,用于确保在多线程环境中对共享资源的访问是原子性的,但它们也存在一些缺点和不足:
- 性能开销:
- 原子操作通常比非原子操作要慢,因为它们需要额外的机制来确保操作的原子性。这可能导致在性能敏感的应用程序中成为瓶颈。
- 原子操作可能导致CPU的缓存行在不同核心之间频繁地来回传输(缓存行弹跳),这称为“缓存行伪共享”(False Sharing)或“缓存行争用”(Cache Line Bouncing),进一步降低性能。
- 忙等待:
- 某些原子操作(如自旋锁)可能会导致线程忙等待,即线程不断地检查某个条件是否成立,而不是让出CPU给其他线程使用。这浪费了CPU资源,尤其是在多核处理器上。
- 不支持复杂操作:
- 原子操作通常只支持简单的、不可分割的操作,如递增、递减、交换等。对于更复杂的操作(如读取-修改-写入操作),可能需要将多个简单的原子操作组合起来,这增加了编程的复杂性和出错的可能性。
- 不支持条件等待:
- 原子操作通常不支持条件等待和通知机制。如果需要在满足特定条件时阻塞线程,并在条件满足时通知线程,那么需要结合其他同步机制(如条件变量)来使用。
- 死锁和优先级反转:
- 虽然原子操作本身不会导致死锁,但在复杂的并发系统中,如果不恰当地使用原子操作(如嵌套锁、循环等待等),仍然可能出现死锁问题。此外,原子操作也可能导致优先级反转问题,尤其是在结合中断禁用或其他同步机制时。
- 有限的应用场景:
- 原子操作主要适用于对共享资源进行简单、原子性操作的场景。对于需要复杂的同步和协作的问题(如读写操作的优化、复杂的数据结构更新等),原子操作可能无法满足需求,需要使用其他更复杂的同步机制。
- 硬件依赖:
- 原子操作指令的具体实现和性能可能因处理器架构和指令集的不同而有所差异。这要求程序员在编写并发代码时需要考虑到目标平台的特性,增加了编程的复杂性。
因此,在使用原子操作指令时,需要权衡其优点和缺点,并根据具体的应用场景和需求来选择合适的同步机制。
原子操作与互斥锁的比较:
原子操作和互斥锁在并发编程中都起着重要作用,但它们在实现机制、适用场景和性能方面有所不同。
- 定义和特性:
- 原子操作:是指在执行期间不会被其他线程中断的操作。它们通常用于对单个内存位置的读取、写入或修改操作,确保对该内存位置的操作是原子的,即不会被其他线程的操作所干扰。原子操作具有不可分割、不可变性和原子性等特性,可以确保对共享数据的操作要么完全执行,要么完全不执行,从而避免数据不一致的问题。
- 互斥锁(Mutex):是一种同步机制,用于保护共享资源,防止并发访问。当一个线程获得了互斥锁后,其他线程必须等待该锁被释放才能继续执行。互斥锁通过保护临界区来防止数据的竞争和不一致性。它的基本操作包括加锁和解锁两个步骤。
- 适用场景:
- 原子操作适用于对单个内存位置进行简单、不可分割的操作的场景。由于原子操作通常比互斥锁具有更高的性能,因此在需要高性能和高并发性的情况下,原子操作是一个很好的选择。
- 互斥锁则更适用于保护较大块的共享资源或执行多个互斥操作的场景。当多个线程需要同时访问和修改共享资源时,互斥锁可以确保在任何时候只有一个线程能够访问该资源,从而避免数据的竞争和不一致性。
- 性能:
- 原子操作通常比互斥锁具有更高的性能,因为它们不需要额外的内存开销(如锁的数据结构)和上下文切换的开销。此外,由于原子操作是不可分割的,因此它们可以减少因线程间竞争而导致的性能下降。
- 互斥锁在保护较大块的共享资源或执行多个互斥操作时可能会带来一些性能开销。此外,当多个线程尝试同时获取互斥锁时,它们可能会被阻塞或进行上下文切换,从而导致性能下降。
- 编程复杂性:
- 原子操作相对简单,通常只需要使用特定的原子操作指令或函数即可。然而,对于复杂的并发问题,可能需要组合多个原子操作来实现所需的同步效果。
- 互斥锁的使用相对复杂一些,需要程序员在代码中显式地加锁和解锁,以确保对共享资源的正确访问。此外,还需要注意避免死锁和优先级反转等问题。
总之,原子操作和互斥锁都是并发编程中常用的同步机制,它们各有优缺点和适用场景。在选择使用哪种机制时,需要根据具体的应用需求和性能要求来权衡利弊。
4. RT-Thread 原子操作 API
RT-Thread 提供了 11 个使用频率较高的原子操作 API。
通过上述API,可以确保多线程环境中,该操作完成后,才会切换到其他线程,保证了操作的完整性。
RT-Thread 原子操作 API | 作用 |
---|---|
rt_atomic_t rt_hw_atomic_load(volatile rt_atomic_t *ptr) | 原子的从 ptr 地址加载/读一个字 |
void rt_atomic_store(volatile rt_atomic_t *ptr, rt_atomic_t val) | 原子的将 val 写入 ptr 地址,不返回当前的值。 |
rt_atomic_t rt_atomic_exchange(volatile rt_atomic_t *ptr, rt_atomic_t val) | 原子的将 ptr 地址处的值替换为 val,并返回当前的值。 |
rt_atomic_t rt_atomic_add(volatile rt_atomic_t *ptr, rt_atomic_t val) | 原子的将 ptr 地址处的值与 val 相加 |
rt_atomic_t rt_atomic_sub(volatile rt_atomic_t *ptr, rt_atomic_t val) | 原子的将 ptr 地址处的值与 val 相减 |
rt_atomic_t rt_atomic_xor(volatile rt_atomic_t *ptr, rt_atomic_t val) | 原子的将 ptr 地址处的值与 val 按位异或 |
rt_atomic_t rt_atomic_and(volatile rt_atomic_t *ptr, rt_atomic_t val) | 原子的将 ptr 地址处的值与 val 按位与 |
rt_atomic_t rt_atomic_or(volatile rt_atomic_t *ptr, rt_atomic_t val) | 原子的将 ptr 地址处的值与 val 按位或 |
rt_atomic_t rt_atomic_flag_test_and_set(volatile rt_atomic_t *ptr) | 原子的将 ptr 地址处的值置 1 |
void rt_atomic_flag_clear(volatile rt_atomic_t *ptr) | 原子的将 ptr 地址处的值清 0 |
rt_atomic_t rt_atomic_compare_exchange_strong(volatile rt_atomic_t *ptr, rt_atomic_t *old, rt_atomic_t new) | 原子的将 ptr 地址处的值与 val 进行比较与交换,并返回比较结果 |
原子操作函数详细的释义:
原子读
rt_atomic_t rt_atomic_load(volatile rt_atomic_t *ptr);
复制错误复制成功
该操作函数的语义为:使用原子操作方式从 ptr 地址指向的 4 字节空间加载一个字。
参数 | 描述 |
---|---|
ptr | 原子对象地址 |
返回值 | 返回 ptr 地址处的 4 字节数据 |
原子写
void rt_atomic_store(volatile rt_atomic_t *ptr, rt_atomic_t val);
复制错误复制成功
该操作函数的语义为:使用原子操作方式将 val 写入 ptr 地址指向的 4 字节空间。
参数 | 描述 |
---|---|
ptr | 原子对象地址 |
val | 期望写入 ptr 地址处的数据 |
返回值 | NULL |
原子数据交换
rt_atomic_t rt_atomic_exchange(volatile rt_atomic_t *ptr, rt_atomic_t val);
复制错误复制成功
该操作函数的语义为:使用原子操作方式将 ptr 地址指向的 4 字节空间的数据交换为 val,返回 ptr 地址处修改前的 4 字节数据。
参数 | 描述 |
---|---|
ptr | 原子对象地址 |
val | 期望交换的数据 |
返回值 | 返回 ptr 地址处交换前的 4 字节数据 |
原子加
rt_atomic_t rt_atomic_add(volatile rt_atomic_t *ptr, rt_atomic_t val);
复制错误复制成功
该操作函数的语义为:使用原子操作方式将 ptr 地址指向的 4 字节数据与 val 相加,将结果写入 ptr 地址指向的 4 字节空间,返回 ptr 地址处修改前的 4 字节数据。
参数 | 描述 |
---|---|
ptr | 原子对象地址 |
val | 期望相加的值 |
返回值 | 返回 ptr 地址处修改前的 4 字节数据 |
原子减
rt_atomic_t rt_atomic_sub(volatile rt_atomic_t *ptr, rt_atomic_t val);
复制错误复制成功
该操作函数的语义为:使用原子操作方式将 ptr 地址指向的 4 字节数据减去 val,将结果写入 ptr 地址指向的 4 字节空间,返回 ptr 地址处修改前的 4 字节数据。
参数 | 描述 |
---|---|
ptr | 原子对象地址 |
val | 期望减去的值 |
返回值 | 返回 ptr 地址处修改前的 4 字节数据 |
原子异或
rt_atomic_t rt_atomic_xor(volatile rt_atomic_t *ptr, rt_atomic_t val);
复制错误复制成功
该操作函数的语义为:使用原子操作方式将 ptr 地址指向的 4 字节数据与 val 进行按位异或,将结果写入 ptr 地址指向的 4 字节空间,返回 ptr 地址处修改前的 4 字节数据。
参数 | 描述 |
---|---|
ptr | 原子对象地址 |
val | 期望异或的值 |
返回值 | 返回 ptr 地址处修改前的 4 字节数据 |
原子与
rt_atomic_t rt_atomic_and(volatile rt_atomic_t *ptr, rt_atomic_t val);
复制错误复制成功
该操作函数的语义为:使用原子操作方式将 ptr 地址指向的 4 字节数据与 val 进行按位相与,将结果写入 ptr 地址指向的 4 字节空间,返回 ptr 地址处修改前的 4 字节数据。
参数 | 描述 |
---|---|
ptr | 原子对象地址 |
val | 期望相与的值 |
返回值 | 返回 ptr 地址处修改前的 4 字节数据 |
原子或
rt_atomic_t rt_atomic_or(volatile rt_atomic_t *ptr, rt_atomic_t val);
复制错误复制成功
该操作函数的语义为:使用原子操作方式将 ptr 地址指向的 4 字节数据与 val 进行按位相或,将结果写入 ptr 地址指向的 4 字节空间,返回 ptr 地址处修改前的 4 字节数据。
参数 | 描述 |
---|---|
ptr | 原子对象地址 |
val | 期望相或的值 |
返回值 | 返回 ptr 地址处修改前的 4 字节数据 |
原子标志检查与置位
rt_atomic_t rt_atomic_flag_test_and_set(volatile rt_atomic_t *ptr);
复制错误复制成功
该操作函数的语义为:对 ptr 地址指向的 4 字节原子标志进行设置,并返回该原子标志对象做设置操作之前的值。若 ptr 地址指向的 4 字节数据之前状态为 0,那么经过此操作之后,该原子标志对象的状态变为了状态 1,并且返回 0 。如果 ptr 地址指向的 4 字节数据之前为状态 1,那么经过此操作之后,它仍然为状态 1,并且返回 1。所以若我们将原子标志对象作为一个“锁”来用的话,可判断这个函数接口的返回值,若返回 0,则说明锁成功,可以对多线程共享对象做相关的修改操作;如果返回的是 状态 1,则该原子标志已经被其他线程占用,需等待释放。
参数 | 描述 |
---|---|
ptr | 原子对象地址,此处地址指向的 4 字节数据只能为 0 或 1 |
返回值 | 设置状态 |
原子标志清除
void rt_atomic_flag_clear(volatile rt_atomic_t *ptr);
复制错误复制成功
该操作函数的语义为:清除标志,将标志清 0,对 ptr 地址指向原子标志进行清零操作。如果我们将原子标志对象用作“锁”的话,那么执行此操作就相当于释放锁。
参数 | 描述 |
---|---|
ptr | 原子对象地址 |
返回值 | NULL |
原子比较与交换
rt_atomic_t rt_atomic_compare_exchange_strong(volatile rt_atomic_t *ptr, rt_atomic_t *old, rt_atomic_t new);
复制错误复制成功
该操作函数的语义为:第一个参数指向原子类型对象;第二个参数指向要进行比较的对象,并且如果比较失败,那么该操作会将原子对象的当前值拷贝到该参数所指向的对象中;第三个参数指定存储到原子对象中的值。 如果比较成功,那么 new 值会被存放到原子对象中,并且返回 1;如果比较失败,那么当前原子对象的值会被拷贝到 old 所指向的对象中,并且返回 0。
参数 | 描述 |
---|---|
ptr | 原子对象地址 |
old | 被比较的对象 |
new | 期望更新的对象 |
返回值 | 比较结果 |
5. 综合示例
在工程中包含rtatomic.h
,然后将示例添加至工程即可进行简单的原子操作验证。
/* 在工程中添加该头文件 */
#include <rtatomic.h>
rt_atomic_t value1 = 10;
rt_atomic_t value2 = 5;
int main(void)
{
/* atomic add */
result = rt_atomic_add(&value1, value2);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
value1 = 10;
value2 = 5;
/* atomic sub */
result = rt_atomic_sub(&value1, value2);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
value1 = 10;
value2 = 5;
/* atomic xor */
result = rt_atomic_xor(&value1, value2);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
value1 = 10;
value2 = 5;
/* atomic or */
result = rt_atomic_or(&value1, value2);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
value1 = 10;
value2 = 5;
/* atomic and */
result = rt_atomic_and(&value1, value2);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
value1 = 10;
value2 = 5;
/* atomic exchange */
result = rt_atomic_exchange(&value1, value2);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
value1 = 10;
value2 = 5;
/* atomic compare and exchange */
result = rt_atomic_compare_exchange_strong(&value1, value2, 6);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
value1 = 10;
/* atomic load */
result = rt_atomic_load(&value1);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
value1 = 10;
value2 = 5;
/* atomic store */
result = rt_atomic_store(&value1, value2);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
value1 = 0;
/* atomic flag test and set */
result = rt_atomic_flag_test_and_set(&value1);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
/* atomic flag clear */
result = rt_atomic_flag_clear(&value1);
rt_kprintf("result = %d value1 = %d value2 = %d\r\n", result, value1, value2);
}