多线程编程中什么时候使用锁和原子操作
在高并发编程中,锁(std::mutex
) 和 原子操作(std::atomic
) 都可以用来保证线程安全,但它们的适用场景不同。选择哪种方式取决于性能、代码复杂度以及对数据一致性的要求。
1. 适用于 std::atomic
(无锁编程)的场景
1.1 适用于简单数据类型的并发修改
如果你需要在多个线程间修改简单的数据类型(如 int
、bool
、指针等),并且这些操作可以用单个 CPU 原子指令完成,则 std::atomic
更合适。
✅ 适用场景:
- 计数器(如请求数、访问次数)
- 标志位(如任务完成标志
std::atomic<bool>
) - 指针交换(如
std::atomic<void*>
共享数据指针)
示例:线程安全的计数器
std::atomic<int> counter{0};
void worker() {
for (int i = 0; i < 1000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
fetch_add()
直接使用 CPU 无锁指令,多个线程可以同时执行,无需等待。
1.2 适用于无锁数据结构
如果你在实现 高性能数据结构,如无锁队列(lock-free queue)或无锁栈(lock-free stack),std::atomic
是必要的。
✅ 适用场景:
- 无锁队列(Lock-Free Queue)
- 无锁栈(Lock-Free Stack)
- 无锁哈希表(Lock-Free HashMap)
示例:无锁栈(Lock-Free Stack)
struct Node {
int data;
Node* next;
};
std::atomic<Node*> head{nullptr};
void push(int value) {
Node* new_node = new Node{value, nullptr};
do {
new_node->next = head.load();
} while (!head.compare_exchange_weak(new_node->next, new_node));
}
compare_exchange_weak()
确保head
在修改前仍然是预期值,如果head
被其他线程改了,会自动重试。
1.3 适用于低延迟场景
在高性能计算或实时系统中,std::atomic
能减少线程同步的开销,避免上下文切换。
✅ 适用场景:
- 实时系统(如音视频处理)
- 游戏引擎(如帧率控制、输入处理)
- 高频金融交易(如撮合交易引擎)
示例:多线程计时器
std::atomic<bool> stop_timer{false};
void timer() {
while (!stop_timer.load()) {
// 执行定时任务
}
}
std::atomic<bool>
可以被多个线程安全地访问,不会产生锁竞争。
2. 适用于 std::mutex
(锁)的场景
尽管原子操作效率更高,但在以下情况下,std::mutex
更适合:
2.1 适用于修改复杂数据结构
如果数据结构较复杂,且多个线程需要同时修改多个变量,那么 std::mutex
更安全。
✅ 适用场景:
- 修改
std::vector
、std::map
等 STL 容器 - 修改多个变量
- 涉及条件变量(
std::condition_variable
)的情况
❌ 不适合用 std::atomic
的例子
struct Data {
int x;
int y;
};
std::atomic<Data> data; // ❌ 错误,不能直接用 std::atomic 处理多个变量
上面代码是错误的,因为 std::atomic
不能保证 x
和 y
在一起更新的原子性。
✅ 正确做法:使用 std::mutex
struct Data {
int x;
int y;
};
Data data;
std::mutex data_mutex;
void update() {
std::lock_guard<std::mutex> lock(data_mutex);
data.x += 1;
data.y += 1;
}
std::mutex
确保x
和y
同时更新,防止竞争条件(race condition)。
2.2 适用于写多读少(写多线程竞争激烈)
如果写操作频繁,std::atomic
可能导致**缓存一致性(cache coherence)**问题,而 std::mutex
可以降低竞争。
✅ 适用场景:
- 高频写入、低频读取的共享资源
- 数据库事务
- 日志系统
示例:日志系统
std::mutex log_mutex;
void log(const std::string& message) {
std::lock_guard<std::mutex> lock(log_mutex);
std::cout << message << std::endl;
}
std::mutex
确保多个线程不会交错输出日志。
2.3 适用于临界区需要长时间执行
如果代码逻辑较复杂,需要执行多个步骤,使用 std::atomic
不现实,此时 std::mutex
更合适。
✅ 适用场景:
- 需要执行多个步骤的临界区
- 持有锁的时间较长
示例:银行账户
struct Account {
int balance;
std::mutex mtx;
};
void transfer(Account& from, Account& to, int amount) {
std::lock_guard<std::mutex> lock(from.mtx);
from.balance -= amount;
std::lock_guard<std::mutex> lock2(to.mtx);
to.balance += amount;
}
std::atomic
无法保证多个账户的一致性,必须用std::mutex
保护。
3. 选择 std::atomic
还是 std::mutex
?
场景 | 适合 std::atomic | 适合 std::mutex |
---|---|---|
共享简单变量(如 int 、bool ) | ✅ | ❌ |
共享指针(如 std::atomic<void*> ) | ✅ | ❌ |
复杂数据结构(如 std::vector ) | ❌ | ✅ |
需要修改多个变量 | ❌ | ✅ |
高性能低延迟应用(如游戏引擎) | ✅ | ❌ |
需要等待(如 std::condition_variable ) | ❌ | ✅ |
数据库事务、日志系统 | ❌ | ✅ |
读多写少的场景 | ✅ | ❌ |
写多读少的场景 | ❌ | ✅ |
4. 结论
std::atomic
适用于无锁编程,适用于高性能、短时锁定的操作(计数器、标志位、指针交换)。std::mutex
适用于修改多个变量、复杂数据结构、需要等待的场景。- 如果可以使用
std::atomic
,优先使用std::atomic
,因为它避免了上下文切换,提高了性能。 - 但当
std::atomic
不能满足一致性需求时(如同时修改多个变量),必须使用std::mutex
。
所以,在实际应用中,先尝试用 std::atomic
,如果无法保证数据完整性,再考虑 std::mutex
! 🚀