原子操作与锁实现
文章目录
- 一、概述
- 1.1 多线程与共享变量
- 1.2 原子操作
- 1.3 原子变量
- 1.4 原子操作的实现与平台相关性
- 二、原子操作常用接口
- 2.1 `is_lock_free()`
- 2.2 `store()`
- 2.3 `load()`
- 2.4 `exchange()`
- 2.5 `compare_exchange_weak` 与 `compare_exchange_strong`
- 2.6 `fetch_add`, `fetch_sub`, `fetch_or`, `fetch_xor` 等
- 三、代码示例
- 3.1 代码整体目的
- 3.2 宏控制
- 3.3 全局变量 `count`
- 3.4 函数 `incrby(int num)`
- 3.5 主函数执行流程
- 3.6 并发问题验证
一、概述
1.1 多线程与共享变量
-
多线程环境
- 多个线程同时运行,共享同一进程的资源(如内存、变量)。
- 线程之间可能同时读写同一变量,导致数据不一致或错误。
-
共享变量的问题
- 如果多个线程同时修改一个变量,结果可能不可预测。
- 例如:两个线程同时对一个计数器进行
+1
操作,最终可能只增加了一次。
-
竞态条件(Race Condition)
- 指程序的输出依赖于线程执行的顺序,结果不确定。
- 解决方法:同步机制,如互斥锁、原子操作等。
机制 | 优点 | 缺点 |
---|---|---|
互斥锁 | 通用、可靠 | 开销大、可能阻塞 |
原子操作 | 轻量、高效、无阻塞 | 仅适用于简单操作 |
1.2 原子操作
-
什么是原子操作?
- 原子操作是指一个或多个步骤组成的操作,这些步骤要么全部执行,要么都不执行,不会被其他线程打断。
- 例如:对一个变量的“读-改-写”操作(如
i++
)如果不是原子的,就可能被中断。
-
为什么需要原子操作?
- 多线程环境下,确保对共享变量的操作在执行时不会被干扰
- 避免竞态条件,确保数据一致性。
- 提高性能:比互斥锁更轻量,不需要上下文切换。
-
原子操作的例子
- 简单的赋值、加法、交换等操作在某些平台上可以是原子的。
- 但复杂的操作(如
i++
)通常不是原子的,需要特殊处理。
1.3 原子变量
-
什么是原子变量?
- 一种特殊类型的变量,对其进行的操作是原子的。
- 通常通过硬件支持或底层指令实现。
-
如何使用原子变量?
- 在C++中可使用
std::atomic<T>
类型。 - 在Java中可使用
AtomicInteger
、AtomicReference
等。
- 在C++中可使用
1.4 原子操作的实现与平台相关性
-
底层实现方式
- 依赖CPU指令集(如x86的
LOCK
前缀指令、ARM的LDREX/STREX指令)。 - 编译器或运行时库会将这些指令封装成原子操作接口。
- 依赖CPU指令集(如x86的
-
平台相关性
- 不同CPU架构对原子操作的支持程度不同。
- 编程语言(如C++、Java)提供了跨平台的原子操作抽象,但性能可能因平台而异。
-
注意事项
- 不是所有操作都能原子化,复杂操作仍需锁机制。
- 原子变量通常只能用于基本类型(如整型、指针)。
二、原子操作常用接口
2.1 is_lock_free()
功能:判断该原子对象是否支持无锁操作(lock-free)。
-
返回值:
true
:支持无锁原子操作,操作完全由 CPU 原子指令实现,不需要互斥锁。false
:需要内部加锁(可能性能较低,但仍保证原子性)。
-
使用场景:
- 性能关键的场合,如果需要尽量减少锁开销。
-
示例:
#include <atomic>
#include <iostream>std::atomic<int> counter;
int main() {if (counter.is_lock_free()) {std::cout << "counter supports lock-free atomic operations\n";} else {std::cout << "counter uses internal locking\n";}
}
注意:不同平台或不同类型(如较大结构体)可能返回不同结果。
2.2 store()
store(T desired, std::memory_order order)
功能:将一个值写入原子对象。
-
参数:
desired
:要存储的新值。order
:内存顺序,常用:memory_order_relaxed
:只保证原子性,不保证顺序。memory_order_release
:保证之前的写操作对其他线程可见。memory_order_seq_cst
:顺序一致性,最严格。
-
示例:
std::atomic<int> counter{0};
counter.store(5, std::memory_order_relaxed); // 设置 counter = 5
注意:store 只修改原子对象的值,如何被其他线程看到,取决于内存顺序。
2.3 load()
load(std::memory_order order)
功能:读取原子对象的当前值。
-
参数:
order
:内存顺序,常用:memory_order_relaxed
:只保证原子性。memory_order_acquire
:确保之后的操作不会被提前到此读之前。memory_order_seq_cst
:全局顺序一致。
-
示例:
std::atomic<int> counter{10};
int val = counter.load(std::memory_order_acquire);
注意:使用 acquire
或 seq_cst
时,可以配合 store(..., release)
保证线程间同步。
2.4 exchange()
exchange(T desired, std::memory_order order)
功能:原子地用新值替换当前值,并返回替换前的旧值。
- 原子性:操作不可分割,读取和写入在其他线程看来是同时完成的。
- 示例:
std::atomic<int> counter{10};
int old = counter.exchange(20, std::memory_order_seq_cst);
std::cout << "old value: " << old << ", new value: " << counter.load() << "\n";
-
适用场景:
- 原子交换值,例如实现锁、标志位或状态切换。
-
注意:
- 返回值是修改前的旧值。
- 与
store
不同,exchange
保证了原子读取+写入。
2.5 compare_exchange_weak
与 compare_exchange_strong
功能:原子比较并交换(CAS, Compare-And-Swap)。
-
共同参数:
T& expected
:期望值,如果当前值等于expected
,就替换;否则把当前值写回expected
。T val
:新的值。memory_order success
:成功替换时的内存顺序。memory_order failure
:替换失败时的内存顺序(通常 <= success)。
-
返回值:
true
:替换成功。false
:替换失败。
-
区别:
weak
:允许在硬件上因“伪失败”而失败,需要循环重试,性能更好。strong
:保证只在值不等时失败,更稳定但可能略慢。
-
示例:
std::atomic<int> counter{10};
int expected = 10;
bool success = counter.compare_exchange_weak(expected, 20, std::memory_order_seq_cst, std::memory_order_relaxed);
if(success) {std::cout << "CAS succeeded, counter = " << counter.load() << "\n";
} else {std::cout << "CAS failed, expected now = " << expected << "\n";
}
注意:
weak
在循环中常用:
int expected = counter.load();
while (!counter.compare_exchange_weak(expected, expected+1)) {// expected 被更新为当前值,循环重试
}
- CAS 是实现很多无锁数据结构的基础。
2.6 fetch_add
, fetch_sub
, fetch_or
, fetch_xor
等
功能:原子地对当前值做运算,并返回修改前的值。
fetch_add(x)
:加上 x。fetch_sub(x)
:减去 x。fetch_or(x)
:按位或。fetch_xor(x)
:按位异或。- 内部保证原子性,可同时读和写。
- 示例:
std::atomic<int> counter{5};
int old = counter.fetch_add(3); // counter += 3, 返回旧值
std::cout << "old: " << old << ", new: " << counter.load() << "\n";
应用场景:
- 原子计数器
- 位操作标志
- 无锁队列或栈的索引更新
注意:
- 同样可以指定内存顺序。
- 返回值是修改前的值。
三、代码示例
通过多线程对一个共享变量 count
进行累加,分别使用 普通变量 / 互斥锁 / 自旋锁 / 原子操作 来保证线程安全,从而对比不同并发控制手段的实现方式。
#include <chrono>
#include <iostream>
#include <thread>
#include <assert.h>// #define USE_ATOMIC 0
#define USE_ATOMIC 1#if USE_MUTEX#include <mutex>std::mutex mtx;int count = 0;
#elif USE_SPINLOCK#include "spinlock.h"using spinlock_t = struct spinlock;spinlock_t spin;int count = 0;
#elif USE_ATOMIC#include <atomic>std::atomic<int> count{0};
#elseint count = 0;
#endifvoid incrby(int num) {for (int i=0; i < num; i++) {
#if USE_MUTEXmtx.lock();++count;mtx.unlock();
#elif USE_SPINLOCKspinlock_lock(&spin);++count;spinlock_unlock(&spin);
#elif USE_ATOMIC// std::this_thread::sleep_for(std::chrono::milliseconds(1));count.fetch_add(1);
#else++count;
#endif}
}int main() {
#ifdef USE_SPINLOCKspinlock_init(&spin);
#endiffor (int i = 0; i < 100; i++) {
#ifdef USE_ATOMICcount.store(0);
#elsecount = 0;
#endifstd::thread a(incrby, 500);std::thread b(incrby, 500);std::thread c(incrby, 500);std::thread d(incrby, 500);a.join();b.join();c.join();d.join();
#ifdef USE_ATOMICif (count.load() != 2000) {
#elseif (count != 2000) {
#endifstd::cout << "i:" << i << " count:" << count << std::endl;break;}}return 0;
}
3.1 代码整体目的
- 多线程环境下,对共享变量
count
进行安全的自增操作。 - 通过不同的同步机制(互斥锁、自旋锁、原子变量)确保
count
的最终结果正确(应该为2000
)。 - 验证并发环境下数据竞争问题以及常见解决方式。
3.2 宏控制
代码中使用了宏定义来切换不同的实现方式:
-
#define USE_ATOMIC 1
- 使用 原子操作(std::atomic),依赖 CPU 的原子指令,无需显式加锁。
-
#if USE_MUTEX
- 使用 互斥锁(std::mutex),线程进入关键区时必须加锁,离开时释放。
-
#elif USE_SPINLOCK
- 使用 自旋锁(spinlock),线程忙等待直到获得锁。
-
#else
- 不加锁、不原子化,会产生数据竞争,结果不确定。
通过修改宏,可以方便地对比不同同步机制的效果与性能。
3.3 全局变量 count
-
普通实现:
int count = 0;
- 不安全,多线程会产生竞态条件(data race)。
-
互斥锁 / 自旋锁:依旧是普通
int
,但通过锁保证访问安全。 -
原子实现:
std::atomic<int> count{0};
- 保证自增操作是原子性的,无需额外锁。
3.4 函数 incrby(int num)
作用:让某个线程执行 num
次 ++count
,并保证线程安全。
不同实现:
-
互斥锁:
mtx.lock(); ++count; mtx.unlock();
- 使用 RAII 会更安全(
std::lock_guard
),此处是手动 lock/unlock。
- 使用 RAII 会更安全(
-
自旋锁:
spinlock_lock(&spin); ++count; spinlock_unlock(&spin);
- 自旋等待,适合临界区很短的场景,否则 CPU 消耗大。
-
原子操作:
count.fetch_add(1);
- 底层依赖硬件原子指令(如 x86 上的
LOCK XADD
),无锁化实现。
- 底层依赖硬件原子指令(如 x86 上的
-
无保护:
++count;
- 会有竞态条件,最终结果可能小于期望值。
3.5 主函数执行流程
- 初始化:若使用自旋锁,需要
spinlock_init(&spin);
。 - 执行循环 100 次,每次做一次完整测试:
count
归零(若是原子变量,用count.store(0)
)。- 启动 4 个线程,每个线程执行
incrby(500)
。 - 主线程等待所有线程结束(
join()
)。 - 检查
count
是否为2000
。- 期望值:
4 * 500 = 2000
。 - 如果不为
2000
,打印出错信息并中断。
- 期望值:
3.6 并发问题验证
-
不加锁、不原子化:出现
count < 2000
,因为多个线程可能同时读取和写入同一个内存地址。- 自增
++count
不是原子操作 - 编译后大致会变成三步:
- 从内存读取
count
到 CPU 寄存器 - 在寄存器里执行
+1
- 把结果写回
count
的内存地址
- 从内存读取
- 出错举例:
假设 `count = 100`,两个线程同时执行 `++count`:线程 A:读 count = 100计算 100 + 1 = 101线程 B:也读 count = 100计算 100 + 1 = 101线程 A:写回 count = 101 线程 B:也写回 count = 101
- 结果:两次自增,最终只增加了 1。
- 自增
-
加锁/原子操作:
- 结果稳定等于
2000
,没有数据丢失。
- 结果稳定等于