在多线程编程中,传统的互斥锁机制虽然简单直观,但会带来额外的性能开销。无锁编程(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 问题、内存顺序和错误处理。