C++ 线程安全类设计详解:从互斥锁到无锁编程 C 线程安全类设计详解从互斥锁到无锁编程一、引言并发时代的类设计挑战在单线程时代类的设计只需要关注逻辑正确性。但在多线程环境下同一个对象的成员函数可能被多个线程同时调用对象的状态可能在任意时刻被并发修改。如果不加以保护数据竞争(Data Race)会导致未定义行为、数据损坏和难以复现的 bug。设计一个线程安全的类意味着要保证在多个线程并发访问时对象始终维持其不变量(Invariant)所有操作表现得像是按某种顺序串行执行。这需要结合互斥锁、原子操作、条件变量等同步原语以及不可变性、线程局部存储等设计策略。二、核心概念速览| 概念 | 说明 || --- | --- || 线程安全 | 多线程并发访问时无需额外同步程序行为仍然正确 || 数据竞争 | 两个线程同时访问同一内存至少一个是写操作且未同步 || 不变量(Invariant) | 对象始终应满足的逻辑条件 || 互斥锁(mutex) | 保证临界区互斥访问的同步原语 || 原子操作(atomic) | 不可分割的单个操作无需锁即可安全并发 || 死锁(Deadlock) | 两个以上线程相互等待对方释放锁 || 条件变量(condition_variable) | 允许线程等待某个条件成立 |三、线程安全类的基本策略3.1 策略一互斥锁保护所有共享状态这是最直接、最常用的策略。在所有可能修改或读取共享状态的成员函数中加锁cpp复制下载#include mutex #include vector #include algorithm #include string #include stdexcept class ThreadSafeVector { private: mutable std::mutex mtx_; // mutable 允许在 const 函数中加锁 std::vectorint data_; public: ThreadSafeVector() default; explicit ThreadSafeVector(std::vectorint initial) : data_(std::move(initial)) { } // 写操作加锁 void push_back(int value) { std::lock_guardstd::mutex lock(mtx_); data_.push_back(value); } // 读操作也要加锁否则可能与写操作产生数据竞争 size_t size() const { std::lock_guardstd::mutex lock(mtx_); // mtx_ 必须是 mutable return data_.size(); } // 获取元素副本值语义避免返回引用 int at(size_t index) const { std::lock_guardstd::mutex lock(mtx_); if (index data_.size()) { throw std::out_of_range(Index out of range); } return data_[index]; // 返回值副本锁释放后副本仍然有效 } // 批量操作在同一个锁保护下完成 std::vectorint snapshot() const { std::lock_guardstd::mutex lock(mtx_); return data_; // 返回整个容器的副本 } // 避免返回内部引用会导致锁释放后引用悬空 // const std::vectorint getData() const { return data_; } // ❌ 危险 }; int main() { ThreadSafeVector vec; // 多线程安全使用 std::thread t1([vec]() { for (int i 0; i 1000; i) { vec.push_back(i); } }); std::thread t2([vec]() { for (int i 0; i 1000; i) { vec.push_back(i 1000); } }); t1.join(); t2.join(); std::cout Final size: vec.size() std::endl; // 2000 }3.2 策略二原子操作无锁对于简单的计数器、标志位等使用std::atomic可以获得比互斥锁更高的性能cpp复制下载#include atomic #include thread #include vector #include iostream class ThreadSafeCounter { private: std::atomicint count_{0}; std::atomicbool initialized_{false}; public: // 原子递增 int increment() { return count_; // 原子操作无需显式加锁 } int decrement() { return --count_; } int get() const { return count_.load(std::memory_order_acquire); } // CAS (Compare-And-Swap) 操作 bool compareAndSet(int expected, int desired) { return count_.compare_exchange_strong(expected, desired); } // 标志位的原子操作 bool tryInitialize() { bool expected false; return initialized_.compare_exchange_strong(expected, true); // 只有一个线程能成功设置 } bool isInitialized() const { return initialized_.load(); } }; int main() { ThreadSafeCounter counter; std::vectorstd::thread threads; for (int i 0; i 10; i) { threads.emplace_back([counter]() { for (int j 0; j 100000; j) { counter.increment(); } }); } for (auto t : threads) t.join(); std::cout Final count: counter.get() std::endl; // 1,000,000 }3.3 策略三读写锁共享互斥当读操作远多于写操作时使用读写锁可以显著提高并发性能cpp复制下载#include shared_mutex #include unordered_map #include string #include optional templatetypename K, typename V class ThreadSafeCache { private: mutable std::shared_mutex rwMtx_; std::unordered_mapK, V cache_; public: // 写操作独占锁 void put(const K key, const V value) { std::unique_lockstd::shared_mutex lock(rwMtx_); // 独占锁 cache_[key] value; } void remove(const K key) { std::unique_lockstd::shared_mutex lock(rwMtx_); cache_.erase(key); } // 读操作共享锁多个线程可同时读取 std::optionalV get(const K key) const { std::shared_lockstd::shared_mutex lock(rwMtx_); // 共享锁 auto it cache_.find(key); if (it ! cache_.end()) { return it-second; } return std::nullopt; } // 存在性检查 bool contains(const K key) const { std::shared_lockstd::shared_mutex lock(rwMtx_); return cache_.count(key) 0; } size_t size() const { std::shared_lockstd::shared_mutex lock(rwMtx_); return cache_.size(); } // 批量读取 std::unordered_mapK, V snapshot() const { std::shared_lockstd::shared_mutex lock(rwMtx_); return cache_; // 返回副本 } };四、线程安全类设计原则4.1 原则一识别和保护不变量cpp复制下载#include mutex #include iostream #include cmath // 差的设计不变量可能被破坏 class BadRectangle { public: double width, height; // 公有可随意修改 double area() const { return width * height; } double diagonal() const { return std::sqrt(width * width height * height); } }; // 问题width 和 height 的修改之间不变量可能被破坏 // 好的设计保护不变量 class GoodRectangle { private: mutable std::mutex mtx_; double width_; double height_; // 不变量width 0 height 0 void validate() const { // 内部一致性检查 } public: GoodRectangle(double w, double h) : width_(w), height_(h) { if (w 0 || h 0) { throw std::invalid_argument(Dimensions must be positive); } } // 原子地修改整个状态 void resize(double newWidth, double newHeight) { if (newWidth 0 || newHeight 0) { throw std::invalid_argument(Dimensions must be positive); } std::lock_guardstd::mutex lock(mtx_); width_ newWidth; height_ newHeight; // 两个字段在同一个临界区内更新保证一致性 } double area() const { std::lock_guardstd::mutex lock(mtx_); return width_ * height_; } double diagonal() const { std::lock_guardstd::mutex lock(mtx_); return std::sqrt(width_ * width_ height_ * height_); } };4.2 原则二避免返回内部引用cpp复制下载#include mutex #include vector #include string // ❌ 危险的 API返回内部引用 class DangerousContainer { mutable std::mutex mtx_; std::vectorstd::string items_; public: const std::string get(size_t index) const { std::lock_guardstd::mutex lock(mtx_); return items_[index]; // 危险锁释放后引用可能失效 } void add(const std::string item) { std::lock_guardstd::mutex lock(mtx_); items_.push_back(item); // 可能使之前的引用失效 } }; // ✓ 安全的 API返回值副本 class SafeContainer { mutable std::mutex mtx_; std::vectorstd::string items_; public: std::string get(size_t index) const { // 返回值 std::lock_guardstd::mutex lock(mtx_); if (index items_.size()) { throw std::out_of_range(Index out of range); } return items_[index]; // 返回副本安全 } // 或者通过回调在锁保护下访问 templatetypename Func void withItem(size_t index, Func func) const { std::lock_guardstd::mutex lock(mtx_); if (index items_.size()) { func(items_[index]); // 在锁保护下访问 } } void add(const std::string item) { std::lock_guardstd::mutex lock(mtx_); items_.push_back(item); } };4.3 原则三注意锁的粒度cpp复制下载#include mutex #include queue #include condition_variable // 锁粒度过大持有锁的时间过长 class CoarseGrainedQueue { std::mutex mtx_; std::queueint queue_; public: void processAll() { std::lock_guardstd::mutex lock(mtx_); // 持有锁的整个处理过程 while (!queue_.empty()) { int item queue_.front(); queue_.pop(); // 长时间操作在锁保护下执行 std::this_thread::sleep_for(std::chrono::milliseconds(100)); processItem(item); } } void push(int item) { std::lock_guardstd::mutex lock(mtx_); queue_.push(item); } void processItem(int item) { /* 处理逻辑 */ } }; // 更好的设计减小锁粒度 class FineGrainedQueue { std::mutex mtx_; std::queueint queue_; std::condition_variable cv_; public: void processAll() { while (true) { int item 0; bool hasItem false; { std::lock_guardstd::mutex lock(mtx_); if (!queue_.empty()) { item queue_.front(); queue_.pop(); hasItem true; } else { break; } } // 锁在这里释放长时间操作在无锁状态下进行 if (hasItem) { processItem(item); // 无锁状态下处理 } } } void push(int item) { { std::lock_guardstd::mutex lock(mtx_); queue_.push(item); } cv_.notify_one(); } void processItem(int item) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } };五、高级线程安全模式5.1 双检查锁定模式Double-Checked Locking用于延迟初始化的线程安全模式cpp复制下载#include mutex #include memory #include atomic class ExpensiveResource { public: ExpensiveResource() { // 昂贵的初始化... } void use() { } }; class LazyInitialized { private: mutable std::mutex mtx_; mutable std::unique_ptrExpensiveResource resource_; mutable std::atomicbool initialized_{false}; public: void use() { // 第一次检查无锁快速路径 if (!initialized_.load(std::memory_order_acquire)) { std::lock_guardstd::mutex lock(mtx_); // 第二次检查持锁防止重复初始化 if (!resource_) { resource_ std::make_uniqueExpensiveResource(); initialized_.store(true, std::memory_order_release); } } resource_-use(); } }; // C11 起更简洁的方式使用 std::call_once class LazyInitializedWithOnce { private: mutable std::once_flag initFlag_; mutable std::unique_ptrExpensiveResource resource_; void initializeResource() const { resource_ std::make_uniqueExpensiveResource(); } public: void use() const { std::call_once(initFlag_, LazyInitializedWithOnce::initializeResource, this); resource_-use(); } };5.2 不可变对象模式设计不可变类是最简单的线程安全策略——没有任何写操作cpp复制下载#include string #include memory // 完全不可变的类天然线程安全 class ImmutablePerson { private: const std::string name_; const int birthYear_; public: ImmutablePerson(std::string name, int birthYear) : name_(std::move(name)), birthYear_(birthYear) { } const std::string getName() const { return name_; } int getBirthYear() const { return birthYear_; } // 修改操作返回新对象不改变原对象 ImmutablePerson withName(const std::string newName) const { return ImmutablePerson(newName, birthYear_); } }; // 多个线程可以安全地共享同一个 ImmutablePerson 对象 // 无需任何同步5.3 线程局部存储cpp复制下载#include mutex #include unordered_map #include memory // 每个线程维护独立的状态 class PerThreadCache { private: // 线程局部存储C11 static thread_local int threadLocalCounter; // 全局共享状态需要保护 static std::mutex globalMtx_; static int globalCounter_; public: void threadLocalIncrement() { threadLocalCounter; // 无需同步每个线程独立 } int getThreadLocalCount() const { return threadLocalCounter; } static void globalIncrement() { std::lock_guardstd::mutex lock(globalMtx_); globalCounter_; } }; thread_local int PerThreadCache::threadLocalCounter 0; std::mutex PerThreadCache::globalMtx_; int PerThreadCache::globalCounter_ 0;六、线程安全类的设计流程图表代码下载全屏七、死锁预防cpp复制下载#include mutex #include thread class BankAccount { int balance_; mutable std::mutex mtx_; int id_; public: BankAccount(int id, int initial) : id_(id), balance_(initial) { } // 危险可能导致死锁 static void unsafeTransfer(BankAccount from, BankAccount to, int amount) { std::lock_guardstd::mutex lock1(from.mtx_); std::lock_guardstd::mutex lock2(to.mtx_); // 如果另一个线程调用 transfer(to, from, ...) // 加锁顺序相反 → 死锁 from.balance_ - amount; to.balance_ amount; } // 安全方案一使用 std::lock 同时锁定多个互斥锁 static void safeTransfer1(BankAccount from, BankAccount to, int amount) { std::lock(from.mtx_, to.mtx_); // 原子地锁定两个互斥锁 std::lock_guardstd::mutex lock1(from.mtx_, std::adopt_lock); std::lock_guardstd::mutex lock2(to.mtx_, std::adopt_lock); from.balance_ - amount; to.balance_ amount; } // 安全方案二C17 scoped_lock static void safeTransfer2(BankAccount from, BankAccount to, int amount) { std::scoped_lock lock(from.mtx_, to.mtx_); // C17 from.balance_ - amount; to.balance_ amount; } // 安全方案三一致的加锁顺序基于 id static void safeTransfer3(BankAccount from, BankAccount to, int amount) { if (from.id_ to.id_) return; // 确保总是先锁 id 较小的账户 BankAccount first (from.id_ to.id_) ? from : to; BankAccount second (from.id_ to.id_) ? to : from; std::lock_guardstd::mutex lock1(first.mtx_); std::lock_guardstd::mutex lock2(second.mtx_); from.balance_ - amount; to.balance_ amount; } int getBalance() const { std::lock_guardstd::mutex lock(mtx_); return balance_; } };八、总结设计线程安全的 C 类需要综合考虑以下方面基本策略互斥锁最通用的方案在成员函数中使用std::mutexstd::lock_guard原子操作适用于简单计数器、标志位使用std::atomic实现无锁读写锁读多写少场景使用std::shared_mutex不可变对象最彻底的线程安全方案无修改操作核心原则保护不变量所有涉及共享状态的修改必须在同一个临界区内完成避免返回内部引用返回值副本而非引用防止锁外的悬空引用控制锁粒度不在持有锁的情况下执行耗时操作一致的加锁顺序防止死锁多锁场景使用std::lock或std::scoped_lock设计检查清单共享状态是否被正确识别和保护每个公共成员函数是否都获取了必要的锁const 成员函数的互斥锁是否声明为mutable是否存在返回内部指针或引用的 API是否有长时间操作在锁保护下执行多个互斥锁的加锁顺序是否一致测试与验证使用 ThreadSanitizer-fsanitizethread检测数据竞争编写并发压力测试模拟多线程高竞争场景测试死锁场景反向加锁顺序线程安全不是事后可以“加上去”的特性而是类设计之初就应该考虑的核心需求。一个好的线程安全类应该让使用者在并发环境下无需额外的外部同步就能安全地使用同时保持尽可能高的并发性能。