在多线程 C++ 程序中,线程安全是最关键的设计要点。常见的同步机制主要有两类:原子操作(std::atomic)和互斥锁(std::mutex)。虽然两者都能保证数据的一致性,但在性能、可读性和使用场景上存在显著差异。本文将结合实际代码示例,分析何时使用 std::atomic,何时使用 std::mutex,以及两者如何协同工作。
1. 原子操作(std::atomic)
1.1 基本概念
std::atomic 是一种无锁编程工具,提供了对原子类型的线程安全访问。它通过硬件指令实现原子性,避免了传统互斥锁所产生的上下文切换和堆栈拷贝开销。
1.2 常见用例
- 计数器:线程安全地递增/递减
- 标志位:例如停止信号、完成标志
- 交换值:
std::atomic::exchange()用于快速更新
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
std::atomic <int> counter{0};
void worker() {
for (int i = 0; i < 10000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::vector<std::thread> workers(8);
for (auto& t : workers) t = std::thread(worker);
for (auto& t : workers) t.join();
std::cout << "Final counter: " << counter.load() << std::endl;
}
1.3 内存序(Memory Order)
std::memory_order_relaxed:最快,但不保证可见性std::memory_order_acquire/std::memory_order_release:常用于生产者-消费者模式std::memory_order_seq_cst:默认强序,最安全但成本最高
2. 互斥锁(std::mutex)
2.1 基本概念
std::mutex 提供了基于锁的同步机制,确保同一时刻只有一个线程访问受保护的资源。它的实现通常依赖操作系统原语(如 pthread_mutex),并可能涉及上下文切换。
2.2 常见用例
- 复杂数据结构:如
std::vector、std::map的读写 - 共享资源:文件句柄、网络连接
- 需要批量操作:一次性锁定多变量
#include <mutex>
#include <vector>
#include <thread>
#include <iostream>
std::vector <int> sharedVec;
std::mutex vecMutex;
void addToVector(int val) {
std::lock_guard<std::mutex> lock(vecMutex);
sharedVec.push_back(val);
}
int main() {
std::vector<std::thread> t(4);
for (int i = 0; i < 4; ++i) {
t[i] = std::thread([i]{
for (int j = 0; j < 100; ++j) addToVector(i * 100 + j);
});
}
for (auto& th : t) th.join();
std::cout << "Vector size: " << sharedVec.size() << std::endl;
}
2.3 std::unique_lock 与 std::scoped_lock
std::unique_lock可在同一作用域内多次锁定/解锁std::scoped_lock可一次性锁定多个互斥量,防止死锁
std::mutex m1, m2;
{
std::scoped_lock lock(m1, m2); // 同时锁定 m1 和 m2
// ...
}
3. 原子 vs 互斥:何时使用哪种
| 场景 | 推荐使用 |
|---|---|
| 需要极致性能,且操作仅为简单读/写/增/减 | std::atomic |
| 操作复杂,需要多变量协同变更 | std::mutex |
| 只需读写一个标志或计数器,且多线程并发量大 | std::atomic |
| 需要对数据结构做批量修改,或有大量锁竞争 | std::mutex |
| 需要保证可见性或顺序(例如生产者-消费者) | 结合 memory_order_acquire/release 的 std::atomic 或 std::mutex |
关键:如果某个变量只需要单个原子操作(读、写、交换),直接使用
std::atomic更简洁且性能更佳;若需要在多线程间同步复杂状态或多个相关变量,互斥锁是更安全的选择。
4. 混合使用:原子 + 互斥的最佳实践
在实际项目中,往往需要将两者结合使用。例如,使用 std::atomic 控制一个 活动线程数,并使用 std::mutex 保护一个共享队列。
#include <atomic>
#include <mutex>
#include <queue>
#include <thread>
#include <iostream>
std::queue <int> taskQueue;
std::mutex queueMutex;
std::atomic <int> activeThreads{0};
void worker() {
while (true) {
int task;
{
std::lock_guard<std::mutex> lock(queueMutex);
if (taskQueue.empty()) break;
task = taskQueue.front();
taskQueue.pop();
}
// 处理任务
std::cout << "Thread " << std::this_thread::get_id() << " processing " << task << std::endl;
}
activeThreads.fetch_sub(1, std::memory_order_relaxed);
}
int main() {
// 初始化任务
for (int i = 0; i < 20; ++i) taskQueue.push(i);
const int numThreads = 4;
activeThreads.store(numThreads, std::memory_order_relaxed);
std::vector<std::thread> workers(numThreads);
for (int i = 0; i < numThreads; ++i)
workers[i] = std::thread(worker);
for (auto& th : workers) th.join();
std::cout << "All tasks completed. Active threads: " << activeThreads.load() << std::endl;
}
activeThreads通过std::atomic记录工作线程数量,避免频繁锁定。- 共享队列
taskQueue用std::mutex保护,保证一次只被一个线程访问。
5. 小结
std::atomic:无锁、低开销,适合简单原子操作(计数器、标志位)。std::mutex:传统锁,适合复杂数据结构或需要批量操作的场景。- 内存序:合理选择
memory_order能显著提升性能。 - 混合使用:在高并发系统中,两者常结合使用,既保持性能,又保证正确性。
掌握这些同步工具后,你可以根据具体需求,灵活选择最合适的方案,构建既高效又可靠的多线程 C++ 程序。