C++ 锁与原子变量的选择指南:从场景到实践
一、引言:两种同步工具,不同的适用边界
在多线程编程中,锁(std::mutex)和原子变量(std::atomic)是两种最基础的同步工具。它们都能保证线程安全,但在适用场景、性能特征和实现复杂度上有本质差异。
选择错误的工具,轻则性能下降,重则导致死锁、数据竞争或逻辑错误。理解两者的适用边界,是写出正确且高效并发代码的关键。
二、核心区别速览
| 维度 | 锁 (mutex) | 原子变量 (atomic) |
| --- | --- | --- |
| 保护范围 | 代码块(临界区) | 单个变量 |
| 操作粒度 | 任意大小 | 单个读写或 RMW 操作 |
| 阻塞行为 | 可能阻塞线程 | 无阻塞 |
| 开销 | 较高(可能系统调用) | 极低(CPU 指令级别) |
| 编程复杂度 | 低 | 高(需理解内存序) |
| 死锁风险 | 有 | 无 |
| 适用数据量 | 多 | 少(单个或少量变量) |
| 典型场景 | 复杂临界区、多变量一致性 | 计数器、标志位、自旋锁 |
三、使用锁的场景
3.1 场景一:需要保护复杂的临界区
当一个操作涉及多个步骤、多个变量,或者需要在整个操作期间保持某个不变条件时,锁是唯一正确的选择。
cpp复制下载
#include <mutex> #include <queue> #include <condition_variable> // 生产者-消费者队列:需要同时保护队列和条件变量 template<typename T> class ThreadSafeQueue { std::queue<T> queue_; mutable std::mutex mtx_; std::condition_variable cv_; size_t maxSize_; public: explicit ThreadSafeQueue(size_t maxSize = 100) : maxSize_(maxSize) { } // 临界区涉及多个操作:检查大小、插入元素、通知 void push(T value) { std::unique_lock<std::mutex> lock(mtx_); // 等待队列有空间——这是一个复杂的条件等待 cv_.wait(lock, [this]() { return queue_.size() < maxSize_; }); queue_.push(std::move(value)); cv_.notify_one(); // 锁在这里释放,保证了 queue_ 和条件变量的状态一致性 } T pop() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this]() { return !queue_.empty(); }); T value = std::move(queue_.front()); queue_.pop(); cv_.notify_one(); return value; } }; // 这种场景无法用原子变量替代——它需要同时保护 queue 和 condition_variable // 并且需要在等待条件成立时不持有锁3.2 场景二:需要保持多个变量之间的一致性
当多个变量之间存在逻辑关联,修改必须作为一个整体进行时,需要锁保护。
cpp复制下载
#include <mutex> #include <string> #include <chrono> // 银行账户:余额和交易历史必须保持一致 class BankAccount { double balance_; std::vector<std::string> history_; mutable std::mutex mtx_; public: bool transferTo(BankAccount& other, double amount) { if (this == &other) return false; // 同时锁定两个账户,避免死锁 std::scoped_lock lock(mtx_, other.mtx_); if (balance_ < amount) return false; balance_ -= amount; other.balance_ += amount; auto now = std::chrono::system_clock::now(); history_.push_back("Transfer out: " + std::to_string(amount)); other.history_.push_back("Transfer in: " + std::to_string(amount)); return true; // balance_ 和 history_ 必须同时更新 // 两个账户的状态也必须保持一致 // 原子变量无法保证这种跨变量的不变性 } double getBalance() const { std::lock_guard lock(mtx_); return balance_; } };3.3 场景三:需要读写锁的高并发读场景
cpp复制下载
#include <shared_mutex> #include <unordered_map> #include <string> // 读多写少的缓存:使用 shared_mutex 允许多个读者并发 template<typename K, typename V> class ConcurrentCache { mutable std::shared_mutex mtx_; std::unordered_map<K, V> cache_; public: // 读操作:共享锁,多个线程可同时读取 std::optional<V> get(const K& key) const { std::shared_lock lock(mtx_); // 共享锁 auto it = cache_.find(key); if (it != cache_.end()) return it->second; return std::nullopt; } // 写操作:独占锁 void put(const K& key, const V& value) { std::unique_lock lock(mtx_); // 独占锁 cache_[key] = value; } // 批量操作:需要在整个操作期间保持独占 void clear() { std::unique_lock lock(mtx_); cache_.clear(); } }; // 原子变量无法提供"共享锁"的概念3.4 场景四:需要等待某个条件成立
cpp复制下载
#include <mutex> #include <condition_variable> #include <atomic> // 条件变量必须配合 mutex 使用,无法用原子变量替代 class OneShotEvent { bool signaled_ = false; std::mutex mtx_; std::condition_variable cv_; public: void wait() { std::unique_lock lock(mtx_); cv_.wait(lock, [this]() { return signaled_; }); } template<typename Rep, typename Period> bool waitFor(const std::chrono::duration<Rep, Period>& timeout) { std::unique_lock lock(mtx_); return cv_.wait_for(lock, timeout, [this]() { return signaled_; }); } void signal() { { std::lock_guard lock(mtx_); signaled_ = true; } cv_.notify_all(); } }; // 原子变量无法让线程"等待"——忙等待会浪费 CPU四、使用原子变量的场景
4.1 场景一:简单计数器和统计
cpp复制下载
#include <atomic> #include <vector> #include <thread> // 请求计数器:只需要原子递增和读取 class RequestCounter { std::atomic<long long> total_{0}; std::atomic<long long> failed_{0}; std::atomic<long long> active_{0}; public: void onRequestStart() { active_.fetch_add(1, std::memory_order_relaxed); } void onRequestEnd() { active_.fetch_sub(1, std::memory_order_relaxed); } void onSuccess() { total_.fetch_add(1, std::memory_order_relaxed); } void onFailure() { failed_.fetch_add(1, std::memory_order_relaxed); onRequestEnd(); } long long getTotal() const { return total_.load(std::memory_order_relaxed); } long long getFailed() const { return failed_.load(std::memory_order_relaxed); } long long getActive() const { return active_.load(std::memory_order_relaxed); } }; // 每个操作都是独立的单变量原子操作 // 使用 relaxed 即可,因为计数器之间不需要严格的顺序保证4.2 场景二:标志位和状态切换
cpp复制下载
#include <atomic> #include <thread> #include <iostream> // 使用原子变量实现线程安全的标志位 class TaskController { std::atomic<bool> running_{false}; std::atomic<bool> paused_{false}; public: void start() { bool expected = false; if (running_.compare_exchange_strong(expected, true)) { std::cout << "Task started" << std::endl; } } void stop() { running_.store(false, std::memory_order_release); } void pause() { paused_.store(true, std::memory_order_release); } void resume() { paused_.store(false, std::memory_order_release); } void workerLoop() { while (running_.load(std::memory_order_acquire)) { if (paused_.load(std::memory_order_acquire)) { std::this_thread::yield(); continue; } // 执行实际工作... } } };4.3 场景三:无锁的“一次初始化”
cpp复制下载
#include <atomic> #include <memory> #include <string> class Logger { std::atomic<bool> initialized_{false}; std::string config_; // 仅初始化线程会写入,后续线程只读 public: void ensureInitialized() { // 快速路径:已初始化,直接返回(无锁) if (initialized_.load(std::memory_order_acquire)) { return; } // 慢速路径:执行初始化 initializeOnce(); } private: void initializeOnce() { // 使用 CAS 确保只有一个线程执行初始化 bool expected = false; if (initialized_.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { // 只有第一个到达的线程会执行这里 config_ = loadConfigFromFile(); // 耗时的初始化操作 } // 其他线程在 CAS 失败后返回,此时 initialized_ 已被设为 true // 使用 acquire 语义确保能看到初始化完成后的 config_ while (!initialized_.load(std::memory_order_acquire)) { std::this_thread::yield(); } } std::string loadConfigFromFile() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); return "config_data"; } };4.4 场景四:无锁数据结构中的节点操作
cpp复制下载
#include <atomic> #include <memory> // 简单的无锁栈:只涉及单节点的指针操作 template<typename T> class LockFreeStack { struct Node { T data; Node* next; Node(const T& val) : data(val), next(nullptr) { } }; std::atomic<Node*> head_{nullptr}; public: void push(const T& value) { Node* newNode = new Node(value); newNode->next = head_.load(std::memory_order_relaxed); // CAS 循环:原子地尝试更新 head while (!head_.compare_exchange_weak( newNode->next, newNode, std::memory_order_release, std::memory_order_relaxed)) { // CAS 失败时重试 } } bool pop(T& result) { Node* oldHead = head_.load(std::memory_order_relaxed); while (oldHead != nullptr) { if (head_.compare_exchange_weak( oldHead, oldHead->next, std::memory_order_acquire, std::memory_order_relaxed)) { result = oldHead->data; delete oldHead; return true; } } return false; } }; // 这类数据结构是原子变量的核心应用场景 // 但实现正确需要深入理解内存序和 ABA 问题五、决策流程
图表代码下载全屏
六、性能对比
cpp复制下载
#include <atomic> #include <mutex> #include <chrono> #include <iostream> #include <thread> #include <vector> const int ITERATIONS = 10000000; // 原子变量版本 void benchmarkAtomic() { std::atomic<long long> counter{0}; auto start = std::chrono::high_resolution_clock::now(); std::vector<std::thread> threads; for (int t = 0; t < 4; ++t) { threads.emplace_back([&counter]() { for (int i = 0; i < ITERATIONS / 4; ++i) { counter.fetch_add(1, std::memory_order_relaxed); } }); } for (auto& t : threads) t.join(); auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); std::cout << "Atomic: " << duration.count() << "ms" << std::endl; } // 互斥锁版本 void benchmarkMutex() { long long counter = 0; std::mutex mtx; auto start = std::chrono::high_resolution_clock::now(); std::vector<std::thread> threads; for (int t = 0; t < 4; ++t) { threads.emplace_back([&counter, &mtx]() { for (int i = 0; i < ITERATIONS / 4; ++i) { std::lock_guard lock(mtx); ++counter; } }); } for (auto& t : threads) t.join(); auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); std::cout << "Mutex: " << duration.count() << "ms" << std::endl; } // 典型结果(高竞争场景): // Atomic: 800ms // Mutex: 2500ms // 原子变量在高竞争场景下比互斥锁快 3 倍左右七、混合使用:当两者需要配合
有些场景需要原子变量和锁配合使用:
cpp复制下载
#include <atomic> #include <mutex> #include <memory> // 使用原子变量优化读多写少场景的性能 class OptimizedCache { std::shared_ptr<const std::string> data_; // 共享的数据 mutable std::mutex writeMtx_; // 仅保护写操作 std::atomic<int> version_{0}; // 版本号,用于快速检查 public: // 读操作:无锁快速路径 std::shared_ptr<const std::string> get() const { auto snapshot = std::atomic_load(&data_); // 原子读取 shared_ptr return snapshot ? snapshot : std::make_shared<const std::string>(""); } // 写操作:需要锁保护 void update(const std::string& newData) { std::lock_guard lock(writeMtx_); auto newPtr = std::make_shared<const std::string>(newData); std::atomic_store(&data_, newPtr); // 原子写入 version_.fetch_add(1, std::memory_order_release); } };八、总结
锁和原子变量的选择可以归纳为以下原则:
- 使用锁 (mutex) 的场景:
- 复杂临界区:需要保护多行代码、多个变量的一致性
- 条件等待:需要
condition_variable等待某个条件成立 - 多读场景:使用
shared_mutex允许多个读者并发 - 跨变量不变性:两个以上变量之间存在逻辑关联
- 简单正确的代码优先:当性能不是绝对关键时,锁更易正确使用
- 使用原子变量 (atomic) 的场景:
- 简单计数器:请求计数、统计、引用计数
- 标志位/状态切换:开始/停止、初始化完成、状态机
- 单变量 RMW:
fetch_add、exchange、CAS - 无锁数据结构:需要极致性能的无锁栈、无锁队列
- 读多写少的优化:配合锁使用,提供快速无锁读路径
- 选择的核心权衡:
- 正确性:锁更易正确使用;原子变量的内存序逻辑容易出错
- 性能:原子变量在低竞争和高竞争场景都有优势(无阻塞、无系统调用)
- 灵活性:锁可以保护任意大小的临界区;原子变量受限于单次操作
- 阻塞行为:锁会阻塞线程(可能上下文切换);原子变量永不阻塞
- 实践建议:
- 默认使用锁,除非明确需要原子变量的性能优势
- 如果临界区只有单变量操作,考虑替换为原子变量
- 如果锁成为性能瓶颈,首先分析竞争程度,再考虑无锁替代方案
- 不要为了"酷"而用无锁——复杂的内存序逻辑是 bug 的温床
一个简单的记忆方式:锁保护代码块,原子变量保护单个值。当需要的保护范围超过一个原子操作能覆盖的范围时,锁是正确且合理的选择。