公司动态
C++ 锁与原子变量的选择指南:从场景到实践
C 锁与原子变量的选择指南从场景到实践一、引言两种同步工具不同的适用边界在多线程编程中锁(std::mutex)和原子变量(std::atomic)是两种最基础的同步工具。它们都能保证线程安全但在适用场景、性能特征和实现复杂度上有本质差异。选择错误的工具轻则性能下降重则导致死锁、数据竞争或逻辑错误。理解两者的适用边界是写出正确且高效并发代码的关键。二、核心区别速览| 维度 | 锁 (mutex) | 原子变量 (atomic) || --- | --- | --- || 保护范围 | 代码块临界区 | 单个变量 || 操作粒度 | 任意大小 | 单个读写或 RMW 操作 || 阻塞行为 | 可能阻塞线程 | 无阻塞 || 开销 | 较高可能系统调用 | 极低CPU 指令级别 || 编程复杂度 | 低 | 高需理解内存序 || 死锁风险 | 有 | 无 || 适用数据量 | 多 | 少单个或少量变量 || 典型场景 | 复杂临界区、多变量一致性 | 计数器、标志位、自旋锁 |三、使用锁的场景3.1 场景一需要保护复杂的临界区当一个操作涉及多个步骤、多个变量或者需要在整个操作期间保持某个不变条件时锁是唯一正确的选择。cpp复制下载#include mutex #include queue #include condition_variable // 生产者-消费者队列需要同时保护队列和条件变量 templatetypename T class ThreadSafeQueue { std::queueT 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_lockstd::mutex lock(mtx_); // 等待队列有空间——这是一个复杂的条件等待 cv_.wait(lock, [this]() { return queue_.size() maxSize_; }); queue_.push(std::move(value)); cv_.notify_one(); // 锁在这里释放保证了 queue_ 和条件变量的状态一致性 } T pop() { std::unique_lockstd::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::vectorstd::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 允许多个读者并发 templatetypename K, typename V class ConcurrentCache { mutable std::shared_mutex mtx_; std::unordered_mapK, V cache_; public: // 读操作共享锁多个线程可同时读取 std::optionalV 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_; }); } templatetypename Rep, typename Period bool waitFor(const std::chrono::durationRep, 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::atomiclong long total_{0}; std::atomiclong long failed_{0}; std::atomiclong 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::atomicbool running_{false}; std::atomicbool 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::atomicbool 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 // 简单的无锁栈只涉及单节点的指针操作 templatetypename T class LockFreeStack { struct Node { T data; Node* next; Node(const T val) : data(val), next(nullptr) { } }; std::atomicNode* 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::atomiclong long counter{0}; auto start std::chrono::high_resolution_clock::now(); std::vectorstd::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_caststd::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::vectorstd::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_caststd::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_ptrconst std::string data_; // 共享的数据 mutable std::mutex writeMtx_; // 仅保护写操作 std::atomicint version_{0}; // 版本号用于快速检查 public: // 读操作无锁快速路径 std::shared_ptrconst std::string get() const { auto snapshot std::atomic_load(data_); // 原子读取 shared_ptr return snapshot ? snapshot : std::make_sharedconst std::string(); } // 写操作需要锁保护 void update(const std::string newData) { std::lock_guard lock(writeMtx_); auto newPtr std::make_sharedconst std::string(newData); std::atomic_store(data_, newPtr); // 原子写入 version_.fetch_add(1, std::memory_order_release); } };八、总结锁和原子变量的选择可以归纳为以下原则使用锁 (mutex) 的场景复杂临界区需要保护多行代码、多个变量的一致性条件等待需要condition_variable等待某个条件成立多读场景使用shared_mutex允许多个读者并发跨变量不变性两个以上变量之间存在逻辑关联简单正确的代码优先当性能不是绝对关键时锁更易正确使用使用原子变量 (atomic) 的场景简单计数器请求计数、统计、引用计数标志位/状态切换开始/停止、初始化完成、状态机单变量 RMWfetch_add、exchange、CAS无锁数据结构需要极致性能的无锁栈、无锁队列读多写少的优化配合锁使用提供快速无锁读路径选择的核心权衡正确性锁更易正确使用原子变量的内存序逻辑容易出错性能原子变量在低竞争和高竞争场景都有优势无阻塞、无系统调用灵活性锁可以保护任意大小的临界区原子变量受限于单次操作阻塞行为锁会阻塞线程可能上下文切换原子变量永不阻塞实践建议默认使用锁除非明确需要原子变量的性能优势如果临界区只有单变量操作考虑替换为原子变量如果锁成为性能瓶颈首先分析竞争程度再考虑无锁替代方案不要为了酷而用无锁——复杂的内存序逻辑是 bug 的温床一个简单的记忆方式锁保护代码块原子变量保护单个值。当需要的保护范围超过一个原子操作能覆盖的范围时锁是正确且合理的选择。