C语言原子操作与无锁编程完全指南

在多线程编程中,传统的互斥锁机制虽然简单直观,但会带来额外的性能开销。无锁编程(Lock-Free Programming)使用原子操作来实现高效的并发数据结构。本文将详细介绍 C11 引入的 stdatomic 头文件以及无锁编程的核心概念。

原子操作基础

原子操作是指不可中断的操作。在多线程环境下,原子操作可以确保操作的完整性,不会被其他线程干扰。C11 标准引入了 头文件,提供了跨平台的原子操作支持。

基本原子类型

atomic_basic.c - 基本原子类型使用
#include 
#include 
#include 

/* 原子整数 */
atomic_int counter = ATOMIC_VAR_INIT(0);
/* 原子布尔 */
atomic_bool ready = ATOMIC_VAR_INIT(false);
/* 原子长整型 */
atomic_long sum = ATOMIC_VAR_INIT(0);

void *worker(void *arg) {
    for (int i = 0; i 100000; i++) {
        atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("Counter: %d\n", atomic_load(&counter));  /* 一定是 200000 */
    return 0;
}

内存顺序模型

C11 定义了六种内存顺序模型,选择合适的内存顺序可以平衡性能与正确性:

  • memory_order_relaxed - 最宽松,仅保证原子性,不保证顺序
  • memory_order_consume - 依赖当前操作的结果才可见
  • memory_order_acquire - 获取之前的所有写入
  • memory_order_release - 释放之后的所有写入对其他线程可见
  • memory_order_acq_rel - 获取+释放语义
  • memory_order_seq_cst - 顺序一致(默认,最安全但最慢)

内存顺序示例

memory_order.c - 不同内存顺序的效果
/* 场景:生产者-消费者模式 */

atomic_int data = 0;
atomic_bool ready = ATOMIC_VAR_INIT(false);

/* 生产者 */
void producer(void) {
    atomic_store_explicit(&data, 42, memory_order_release);
    atomic_store_explicit(&ready, true, memory_order_release);
}

/* 消费者 - 使用 acquire 确保看到 release 前的写入 */
void consumer(void) {
    while (!atomic_load_explicit(&ready, memory_order_acquire)) {
        sched_yield();
    }
    /* 这里的 data 一定是 42,因为 release-acquire 同步 */
    int val = atomic_load_explicit(&data, memory_order_acquire);
    printf("Got: %d\n", val);
}

无锁计数器实现

基于 CAS(Compare-And-Swap)操作实现无锁计数器:

lock_free_counter.c - 无锁计数器
#include 
#include 

typedef struct {
    atomic_int value;
} LockFreeCounter;

void counter_init(LockFreeCounter *c) {
    atomic_init(&c->value, 0);
}

void counter_inc(LockFreeCounter *c) {
    /* 简单版本:直接原子加法 */
    atomic_fetch_add(&c->value, 1);
}

int counter_get(LockFreeCounter *c) {
    return atomic_load(&c->value);
}

/* 基于 CAS 的条件更新(更通用) */
int counter_add_if(LockFreeCounter *c, int delta, int expected) {
    int current = atomic_load(&c->value);
    do {
        if (current != expected) return 0;  /* 条件不满足 */
    } while (!atomic_compare_exchange_weak(&c->value, ¤t, current + delta));
    return 1;  /* 成功 */
}

无锁栈实现

使用原子操作实现线程安全的无锁链表栈:

lock_free_stack.c - 无锁栈
#include 
#include 
#include 

typedef struct Node {
    int value;
    struct Node *next;
} Node;

typedef struct {
    _Atomic Node *head;
} LockFreeStack;

void stack_init(LockFreeStack *s) {
    atomic_init(&s->head, NULL);
}

void stack_push(LockFreeStack *s, int value) {
    Node *new_node = malloc(sizeof(Node));
    new_node->value = value;

    Node *old_head;
    do {
        old_head = atomic_load(&s->head);
        new_node->next = old_head;
    } while (!atomic_compare_exchange_weak(&s->head, &old_head, new_node));
}

int stack_pop(LockFreeStack *s, int *out) {
    Node *old_head;
    do {
        old_head = atomic_load(&s->head);
        if (old_head == NULL) return 0;  /* 空栈 */
    } while (!atomic_compare_exchange_weak(&s->head, &old_head, old_head->next));

    *out = old_head->value;
    free(old_head);
    return 1;
}

ABA 问题与解决方案

CAS 操作可能导致 ABA 问题:线程 A 读取值 X,线程 B 将值改为 Y 再改回 X,线程 A 的 CAS 仍会成功,但实际上中间发生了其他操作。

使用双宽 CAS(Double-Width CAS)

利用 128 位原子类型解决 ABA 问题:

aba_solution.c - 使用标记指针解决 ABA 问题
#include 
#include 

/* 将指针和计数器打包 */
typedef struct TaggedPtr {
    void *ptr;
    uintptr_t tag;
} TaggedPtr;

typedef struct {
    _Atomic TaggedPtr ptr;
} TaggedPtrSlot;

/* 原子地更新带标记的指针 */
void tagged_ptr_set(TaggedPtrSlot *slot, void *ptr) {
    TaggedPtr old, new_ptr;
    do {
        old = atomic_load(&slot->ptr);
        new_ptr.ptr = ptr;
        new_ptr.tag = old.tag + 1;  /* 增加版本号 */
    } while (!atomic_compare_exchange_weak(&slot->ptr, &old, new_ptr));
}

性能对比

原子操作 vs 互斥锁的性能对比:

  • 单线程简单操作 - 两者差异可忽略
  • 高竞争场景 - 无锁方案可提供数倍吞吐量
  • 低竞争场景 - 互斥锁可能因缓存更有效而更快
  • 读多写少 - 原子操作配合内存顺序优化效果最佳

常见陷阱

  • 过度使用_seq_cst - 默认顺序一致可能影响性能
  • ABA 问题忽视 - 链表等数据结构必须考虑
  • 内存泄漏 - 无锁结构的节点释放需要额外小心
  • 死循环风险 - CAS 失败时需要适当退避

总结

无锁编程是高性能并发系统的核心技术。使用 stdatomic 可以实现高效的数据共享,内存顺序模型允许我们在正确性和性能之间做权衡。但无锁编程复杂度高,实现时需要仔细考虑 ABA 问题、内存顺序和错误处理。