简介
原子变量
原子变量(Atomic variables)是指在多线程环境下可以被多个线程安全地访问和修改的变量,而不需要加锁。这些变量提供了原子操作,确保在操作期间,变量的状态不会被其他线程打断或修改。
原子操作
原子操作是指某个操作要么完全执行,要么完全不执行。也就是说,原子操作是不可分割的,不会被中断。常见的原子操作包括对变量的增加、减少、设置和获取等。
作用
在多线程编程中,当多个线程同时访问共享数据时,常常会发生竞态条件(race condition),导致不一致的结果。例如,两个线程可能同时读取同一个变量,并且都尝试写入该变量,这样就会发生冲突,最终的结果不可预测。原子变量通过确保在更新变量时不会发生竞争条件,从而保证线程安全。
C语言中的原子变量
C 语言标准库(C11)通过
atomic类型
atomic 类型是 C11 引入的一个类型,可以对其进行原子操作,避免了加锁的开销。C11 中定义了一个 atomic 类型,用于表示原子类型的变量。常见的原子变量类型包括 atomic_int、atomic_bool、atomic_char 等。
原子初始化
使用 atomic_init 函数初始化原子变量。
#include
atomic_int x;
atomic_init(&x, 10); // 将原子变量 x 初始化为 10
原子读取
使用 atomic_load 函数读取原子变量的值。
int value = atomic_load(&x); // 获取原子变量 x 的值
原子写入
使用 atomic_store 函数将值写入原子变量。
atomic_store(&x, 20); // 将原子变量 x 的值设置为 20
原子加法和减法
使用 atomic_fetch_add 和 atomic_fetch_sub 进行原子加法和减法操作。
atomic_fetch_add(&x, 5); // x = x + 5,操作是原子的
atomic_fetch_sub(&x, 3); // x = x - 3,操作是原子的
原子比较并交换(CAS)
原子比较并交换(CAS) atomic_compare_exchange_strong 或 atomic_compare_exchange_weak 是原子变量最常用的操作之一。它尝试将一个原子变量的值与预期值进行比较,如果相等,则将其修改为新值。它是原子操作的基础,用于实现锁-free 数据结构和算法。
atomic_int old = 10;
atomic_compare_exchange_strong(&x, &old, 20); // 如果 x == old,则将 x 设置为 20
原子操作的实现方式:
加载:首先加载当前原子变量的值。
比较:比较加载的值与预期值(即传入的参数)是否相等。
交换:如果相等,使用原子交换指令将原子变量的值替换为新的值。
返回:返回原子变量的旧值,并表示是否交换成功。
bool atomic_compare_exchange_strong(atomic_int* obj, int* expected, int desired) {
int old = atomic_load(obj);
if (old == *expected) {
atomic_store(obj, desired);
return true;
} else {
*expected = old;
return false;
}
}
示例
以下是一个简单的示例,展示如何使用原子变量来安全地计数,而不需要使用锁:
#include
#include
#include
atomic_int counter = 0; // 声明一个原子变量
void* increment(void* arg) {
for (int i = 0; i < 1000000; i++) {
atomic_fetch_add(&counter, 1); // 原子加 1
}
return NULL;
}
int main() {
pthread_t t1, t2;
// 创建两个线程
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
// 等待两个线程完成
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 输出最终的计数值
printf("Final counter value: %d\n", atomic_load(&counter));
return 0;
}
原子变量 vs 锁
性能:原子操作比锁的性能要好,因为它们避免了线程上下文切换和锁竞争的开销。
复杂性:锁机制相对简单直观,原子操作需要你理解更多的并发控制细节。
应用场景:原子操作适合简单的数值更新,如计数器、标志位等;而锁适用于需要更多复杂逻辑和状态维护的场景。
底层实现原理
这些类型实际上是通过编译器提供的特定扩展来实现的,编译器生成的底层代码会调用相应的汇编指令或操作系统的原子操作接口。
内存屏障(Memory Barrier): 在多线程编程中,内存屏障或内存顺序是原子操作的重要组成部分。它的作用是保证特定操作的执行顺序,不被编译器或处理器重排。例如:
写屏障:确保之前的写操作在屏障之前完成。
读屏障:确保屏障之后的读操作不会被重排到屏障之前。
原子操作通常会伴随着内存屏障(也称为“序列点”)来确保正确的内存访问顺序。这些内存屏障通过编译器或底层硬件提供。
硬件支持的原子指令: 多数现代处理器(如 x86、ARM)提供了硬件原子操作的支持,如:
x86:LOCK CMPXCHG(比较并交换)指令,保证了在多个处理器核心之间共享数据时的原子性。
ARM:类似的原子交换指令如 LDREX(加载并排他)和 STREX(存储并排他)。
这些指令通过 LOCK 前缀来确保内存操作不会被中断或重新排序。