Linux C++高并发开发:线程安全单例、STL容器与锁机制实战解析

发布时间:2026/7/12 5:13:21

Linux C++高并发开发:线程安全单例、STL容器与锁机制实战解析 1. 项目概述线程周边话题的深度整合与实践在Linux C后端开发中处理多线程就像在繁忙的十字路口指挥交通任何一个环节的疏忽都可能导致数据“撞车”——也就是我们常说的线程安全问题。最近在复盘和设计一些高并发服务时我重新梳理了几个紧密相关且极易出错的“线程周边”核心话题如何实现一个真正线程安全的单例模式、STL容器和智能指针在多线程环境下的“坑”与“解”、Linux下各种锁的适用场景与性能权衡以及如何用读者写者模型优雅地解决特定并发场景。这些知识点散落在各处但实际项目中它们往往交织在一起共同决定了程序的稳定性和性能上限。这篇文章我就以一个一线开发者的视角把这些话题串起来结合代码实例和性能测试数据深入聊聊其中的原理、陷阱和最佳实践。无论你是正在面试准备还是在实际开发中遇到了诡异的并发bug希望这篇整合后的干货能给你带来清晰的思路和可直接复用的方案。2. 线程安全的单例模式从原理到工业级实现单例模式大概是设计模式里最出名也最常用的一种尤其是在需要全局唯一配置管理、日志系统或连接池的场景。但在多线程环境下一个粗糙的单例实现就是一颗定时炸弹。线程安全单例的核心挑战在于如何保证在多线程同时调用获取实例的方法时实例只被创建一次且初始化过程是安全的。2.1 饿汉与懒汉两种模式的本质区别首先得理清饿汉式Eager Initialization和懒汉式Lazy Initialization的根本逻辑这决定了你的程序启动速度和内存占用。饿汉式的核心思想是“我全都要现在就要”。它在程序启动、静态变量初始化阶段早于main函数执行就完成单例对象的构造。这种方式天生是线程安全的因为初始化发生在任何线程启动之前。class EagerSingleton { private: static EagerSingleton instance; // 静态成员声明 EagerSingleton() {} // 私有构造函数 EagerSingleton(const EagerSingleton) delete; // 禁止拷贝 EagerSingleton operator(const EagerSingleton) delete; // 禁止赋值 public: static EagerSingleton GetInstance() { return instance; // 直接返回已初始化的实例 } }; // 关键在类外定义并初始化静态成员 EagerSingleton EagerSingleton::instance;注意饿汉式的instance必须作为类的静态成员在.cpp文件中定义。如果只在类内声明链接时会报未定义错误。它的优点是绝对安全、代码简单缺点是无论你用不用对象都被创建可能拖慢启动速度如果构造过程依赖其他未就绪的全局状态还会引发初始化顺序问题。懒汉式则信奉“按需分配”。只有第一次调用GetInstance()时才去创建对象。这避免了不必要的资源占用但把线程安全的复杂性完全暴露了出来。我们接下来的讨论主要围绕如何实现一个线程安全的懒汉单例。2.2 经典懒汉的线程安全问题与双重检查锁定最原始的懒汉实现是灾难性的// 线程不安全的经典错误示例 Singleton* Singleton::instance_ nullptr; Singleton* Singleton::GetInstance() { if (instance_ nullptr) { // 检查1 instance_ new Singleton(); // 构造 } return instance_; }假设线程A和B同时执行到检查1都发现instance_为空那么它们会先后执行new操作导致单例被构造两次内存泄漏且后续行为未定义。最直观的修复是加锁std::mutex mtx; Singleton* Singleton::GetInstance() { std::lock_guardstd::mutex lock(mtx); // 每次调用都加锁 if (instance_ nullptr) { instance_ new Singleton(); } return instance_; }这虽然安全了但性能堪忧。每次获取实例这是一个非常频繁的操作都要串行通过锁在高并发下会成为瓶颈。于是双重检查锁定Double-Checked Locking, DCP登场了。其思想是将加锁的粒度缩小只有第一次创建时才需要锁。std::atomicSingleton* Singleton::instance_{nullptr}; // C11后使用atomic std::mutex Singleton::mtx_; Singleton* Singleton::GetInstance() { Singleton* tmp instance_.load(std::memory_order_acquire); // 第一次检查无锁 if (tmp nullptr) { std::lock_guardstd::mutex lock(mtx_); tmp instance_.load(std::memory_order_relaxed); // 第二次检查持有锁时 if (tmp nullptr) { tmp new Singleton(); instance_.store(tmp, std::memory_order_release); } } return tmp; }这里有几个关键点使用std::atomicinstance_指针必须用原子变量包装保证线程间可见性。早期的DCP在C11之前因为内存模型问题是有缺陷的可能由于指令重排导致其他线程看到未初始化完全的对象atomic配合合适的内存序memory_order_acquire/release解决了这个问题。两次判空第一次无锁检查用于性能优化绝大多数时候实例已存在直接返回。第二次在锁内检查是防止多个线程同时通过第一次检查后在锁上排队导致重复创建。锁的选择这里用了std::mutex在实例创建这个极低频事件上其开销可以接受。2.3 C11之后的终极方案局部静态变量与Magic Static实际上在C11及以后的标准中有更简洁、更安全的方案它利用了**静态局部变量初始化线程安全Magic Static**的特性class Singleton { public: static Singleton GetInstance() { static Singleton instance; // C11保证此初始化是线程安全的 return instance; } private: Singleton() default; ~Singleton() default; Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; };这是目前工业界公认的最佳实践。C11标准明确规定如果控制流在变量初始化时首次进入声明则并发执行应等待初始化完成。编译器在底层为我们生成了类似双重检查锁的线程安全代码。它的优点是零心智负担、代码极简、安全可靠。除非你有极端性能要求需要自定义内存对齐或placement new否则都应优先采用这种方式。实操心得我曾在一个旧项目中重构单例将手写的DCP全部替换为Magic Static。不仅代码量减少了70%而且通过helgrind线程检查工具发现原有的DCP实现在某些边缘路径下仍有微小的数据竞争风险源于自定义的原子操作不够严谨而Magic Static则彻底消除了这类隐患。对于现代C项目这几乎是唯一的选择。3. STL容器与智能指针的线程安全剖析实现了安全的单例我们常会在单例内部或其它地方使用STL容器如std::map,std::vector和智能指针如std::shared_ptr来管理数据。这时另一个误区出现了认为使用了这些“现代”组件就自动获得了线程安全。大错特错3.1 STL容器的线程安全级别读与写的界限C标准对STL容器线程安全性的保证非常有限可以概括为多个线程同时读同一个容器是安全的。多个线程同时写不同的容器是安全的。如果一个线程在写其他线程无论是读还是写同一个容器都是不安全的除非用户自行同步。这意味着像push_back、insert、erase、operator[]非const版本这些修改容器的操作都不是原子的。两个线程同时push_back极有可能破坏vector的内部结构如导致迭代器失效或内存重复释放。一个常见的错误场景是在“线程安全”的单例里用一个std::map做缓存然后多个线程并发地去find和insert。// 错误示例看似安全的单例内部容器操作不安全 class ConfigCache { public: static ConfigCache GetInstance() { /* Magic Static 实现 */ } std::string GetConfig(const std::string key) { auto it config_map_.find(key); // 读操作 if (it ! config_map_.end()) { return it-second; } else { // 模拟从数据库加载 std::string value loadFromDB(key); config_map_[key] value; // 写操作非线程安全 return value; } } private: std::unordered_mapstd::string, std::string config_map_; };即使GetInstance是线程安全的但GetConfig方法内部的find和operator[]组合在一起在没有锁保护的情况下如果两个线程同时查询同一个不存在的key会导致对config_map_的并发写行为未定义。解决方案是使用锁进行同步std::string GetConfig(const std::string key) { { std::shared_lockstd::shared_mutex lock(mutex_); // 读锁 auto it config_map_.find(key); if (it ! config_map_.end()) { return it-second; } } // 未找到需要加载 std::string value loadFromDB(key); { std::unique_lockstd::shared_mutex lock(mutex_); // 写锁 // 再次检查防止在获取写锁前其他线程已经加载了 auto it config_map_.find(key); if (it config_map_.end()) { config_map_[key] value; } else { value it-second; // 使用其他线程加载的值 } } return value; } private: std::unordered_mapstd::string, std::string config_map_; mutable std::shared_mutex mutex_; // 使用读写锁优化读多写少场景这里引入了std::shared_mutexC17和std::shared_lock/std::unique_lock实现了读者写者锁允许多个读线程并发优化了性能。这是处理STL容器线程安全的典型模式。3.2 智能指针的线程安全shared_ptr的陷阱与救赎智能指针特别是std::shared_ptr其线程安全模型常常被误解。需要明确两个层次的安全控制块引用计数的安全std::shared_ptr的引用计数操作是原子的因此多个线程同时拷贝、赋值、析构同一个shared_ptr实例即操作其控制块是安全的。这保证了对象不会被提前销毁。指向对象数据的安全std::shared_ptr并不保证其指向的对象的线程安全。多个线程通过不同的shared_ptr副本去访问读/写同一个对象需要用户自己同步。最危险的场景是对shared_ptr本身进行写操作。例如// 全局或共享的shared_ptr std::shared_ptrMyObject global_ptr; // 线程A void thread_a() { global_ptr std::make_sharedMyObject(args...); // 写操作 } // 线程B void thread_b() { if (global_ptr) { // 读操作 global_ptr-DoSomething(); // 通过指针访问对象 } }thread_a中的赋值和thread_b中的读取if (global_ptr)如果同时发生不是原子操作可能导致thread_b看到的是一个不完整的指针值比如正在从旧值切换到新值的中间状态进而引发崩溃。即使引用计数安全这个指针本身的读写也不是原子的。安全的使用方式有两种方式一每个线程使用自己的shared_ptr副本。通过值传递shared_ptr每个线程操作的是自己的副本互不干扰。这适用于对象生命周期由某个主线程管理其他线程只读的场景。方式二使用std::atomicstd::shared_ptrTC20或std::atomic_load/std::atomic_storeC11。这是为了安全地在线程间传递shared_ptr所有权。// C11/14 方式 std::shared_ptrMyObject global_ptr; std::mutex ptr_mutex; // 或者用原子操作 void update_ptr() { auto new_ptr std::make_sharedMyObject(); std::atomic_store(global_ptr, new_ptr); // 原子存储 } void use_ptr() { auto local_ptr std::atomic_load(global_ptr); // 原子加载 if (local_ptr) { local_ptr-DoSomething(); } }C20直接提供了std::atomicstd::shared_ptrT特化用起来更直观。踩坑记录在一次网络服务调试中我们遇到一个随机崩溃崩溃栈指向一个看似合法的shared_ptr调用成员函数。最终用AddressSanitizer和线程检查器发现问题正源于一个全局配置shared_ptr被多个工作线程并发读写非原子操作。将shared_ptr的读写用std::atomic_store/load包裹后问题消失。这个坑告诉我们智能指针解决了内存泄漏但没解决并发数据竞争。4. Linux下常见锁的选型与性能权衡既然知道了数据共享需要同步锁就成了必备工具。但锁不是一把万能钥匙不同的锁适用于不同的场景选错了性能差距巨大。4.1 互斥锁Mutex最通用的卫士std::mutex或其底层Pthread的pthread_mutex_t是最基础的互斥锁它保证了同一时间只有一个线程能进入临界区。特点简单、可靠、开销相对较小。适用场景临界区代码执行时间较短且竞争不特别激烈的场景。注意事项死锁连续两次锁定同一个非递归mutex会导致死锁。如果需要重入使用std::recursive_mutex。RAII务必使用std::lock_guard或std::unique_lock来管理锁确保异常安全。std::mutex mtx; { std::lock_guardstd::mutex lock(mtx); // 构造时加锁析构时解锁 // 临界区操作 } // 自动解锁4.2 自旋锁Spinlock为短等待而生的急脾气自旋锁在获取锁时如果锁已被占用它不会让出CPU而是循环“自旋”检查锁的状态直到锁可用。特点避免了线程上下文切换的开销但会空耗CPU。适用场景临界区执行时间极短通常小于几次上下文切换的时间比如几十到几百个时钟周期且多核CPU环境。常用于内核或极高性能的用户态基础库。C实现C11没有标准自旋锁但可以用std::atomic_flag实现一个简单的版本class Spinlock { std::atomic_flag flag ATOMIC_FLAG_INIT; public: void lock() { while (flag.test_and_set(std::memory_order_acquire)) { // 可加入__builtin_ia32_pause() (x86)或std::this_thread::yield()减轻CPU压力 } } void unlock() { flag.clear(std::memory_order_release); } };性能对比心得在一个高频计数器比如QPS统计的场景下我测试过std::mutex和自旋锁。当临界区只是counter这一条指令时在8核机器上自旋锁的性能大约是mutex的2-3倍。但当临界区稍微复杂一点比如多了几次内存访问或者竞争激烈时自旋锁的性能会急剧下降因为所有等待线程都在疯狂空转消耗CPU。结论除非你非常确定临界区极小且竞争不极端否则优先用mutex。4.3 读写锁Read-Write Lock读者写者模型的利器读写锁区分了读操作和写操作。它允许多个读线程同时持有锁共享但只允许一个写线程持有锁独占且读写互斥。特点极大优化了读多写少场景的性能。C实现C14引入了std::shared_timed_mutexC17引入了更基础的std::shared_mutex。std::shared_mutex rw_mutex; // 读线程 { std::shared_lockstd::shared_mutex lock(rw_mutex); // 共享锁 // 可以并发执行的读操作 } // 写线程 { std::unique_lockstd::shared_mutex lock(rw_mutex); // 独占锁 // 互斥的写操作 }适用场景配置信息缓存、服务注册发现中心、大部分时间查询偶尔更新的数据字典等。陷阱要小心“写者饥饿”问题。如果读锁持续不断写线程可能永远无法获取锁。一些实现如Pthread的pthread_rwlock_t可以通过设置属性来优先考虑写者。4.4 条件变量Condition Variable线程间的通知机制条件变量std::condition_variable本身不是锁它用于阻塞一个或多个线程直到某个条件被满足由其他线程通知。它总是与一个互斥锁std::mutex配合使用。核心操作wait,notify_one,notify_all。经典模式——生产者消费者队列std::queueData queue; std::mutex mtx; std::condition_variable cv; // 生产者 void producer() { Data data produce_data(); { std::lock_guardstd::mutex lock(mtx); queue.push(data); } cv.notify_one(); // 通知一个消费者 } // 消费者 void consumer() { while (true) { std::unique_lockstd::mutex lock(mtx); // 必须用循环检查条件防止虚假唤醒 cv.wait(lock, []{ return !queue.empty(); }); Data data queue.front(); queue.pop(); lock.unlock(); // 尽早释放锁 process_data(data); } }关键点wait调用时会原子地释放锁并将线程挂起。被唤醒后会重新获取锁。必须使用循环和谓词lambda来检查条件因为可能存在“虚假唤醒”spurious wakeup即线程没有收到notify也可能从wait返回。通常先修改共享数据如queue.push再通知notify。通知操作可以放在锁外以减小锁的粒度提升性能。4.5 无锁编程Lock-Free高阶玩家的领域无锁编程通过原子操作std::atomic和内存序Memory Order来构建并发数据结构完全避免互斥锁。例如无锁队列、无锁栈。优点可扩展性好避免了死锁、优先级反转、锁竞争带来的性能抖动。缺点极其复杂正确性难以证明调试困难。对内存序的理解要求极高memory_order_relaxed,acquire,release,seq_cst等。建议除非你是并发库的开发者或者有确凿的性能瓶颈证明锁是瓶颈否则不要轻易尝试自己实现无锁数据结构。可以使用成熟的库如folly::AtomicHashMap或boost::lockfree::queue。锁选型速查表锁类型核心特点典型适用场景注意事项互斥锁 (Mutex)独占访问简单可靠通用临界区保护竞争不极端注意死锁使用RAII管理自旋锁 (Spinlock)忙等待避免上下文切换临界区极短1us多核竞争激烈时CPU空转严重读写锁 (RWLock)读共享写独占读多写少的数据结构如缓存注意写者饥饿问题条件变量 (CondVar)线程等待/通知机制生产者-消费者、线程池任务调度必须配合谓词防止虚假唤醒无锁 (Lock-Free)基于原子操作无阻塞高性能基础组件队列、计数器实现复杂慎用5. 读者写者模型理论与高性能实现读者写者问题是并发编程中的一个经典模型它抽象了这样一种场景一份共享数据如数据库、缓存可以被多个“读者”线程同时读取但“写者”线程在修改数据时必须独占访问且读写不能同时进行。我们前面提到的读写锁就是实现读者写者模型最直接的同步原语。5.1 模型的核心规则与优先级策略模型规则很简单允许多个读者同时读。只允许一个写者写。写者工作时既不能有其他写者也不能有读者。但这里引出一个关键问题读者和写者谁该优先这决定了在竞争时的行为。读者优先只要有一个读者在读后续的读者可以直接加入阅读。这可能导致写者饥饿如果一直有读者来。写者优先如果有写者在等待那么新到来的读者必须等待直到所有等待的写者完成。这可能导致读者饥饿。公平策略通常按照FIFO先来先服务的顺序或者通过设置最大读者数来平衡。Linux的pthread_rwlock_t可以通过PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP等属性来一定程度上倾向写者但并非完全公平。完全的公平策略通常需要自己用条件变量和计数器来实现。5.2 基于条件变量和计数器的公平读写锁实现下面展示一个相对公平的读写锁实现它避免了某一方的极端饥饿class FairRWLock { public: void ReadLock() { std::unique_lockstd::mutex lock(mtx_); // 等待条件没有写者正在写且没有写者在等待写者优先的公平性 read_cv_.wait(lock, [this]() { return writing_ 0 waiting_writers_ 0; }); reading_; } void ReadUnlock() { std::unique_lockstd::mutex lock(mtx_); if (--reading_ 0) { // 所有读者都读完了可以唤醒一个写者 write_cv_.notify_one(); } } void WriteLock() { std::unique_lockstd::mutex lock(mtx_); waiting_writers_; // 登记一个等待的写者 // 等待条件没有读者在读也没有其他写者在写 write_cv_.wait(lock, [this]() { return reading_ 0 writing_ 0; }); --waiting_writers_; writing_ 1; } void WriteUnlock() { std::unique_lockstd::mutex lock(mtx_); writing_ 0; if (waiting_writers_ 0) { // 优先唤醒写者写者优先 write_cv_.notify_one(); } else { // 没有写者等待唤醒所有读者 read_cv_.notify_all(); } } private: std::mutex mtx_; std::condition_variable read_cv_; std::condition_variable write_cv_; int reading_ 0; int writing_ 0; int waiting_writers_ 0; // 等待的写者数量用于实现公平性 };这个实现体现了“写者优先”的公平策略当有写者在等待时waiting_writers_ 0新来的读者会被阻塞。这保证了写者不会无限期等待。解锁时也优先唤醒写者。5.3 实战应用一个线程安全的配置管理类让我们把单例模式、STL容器线程安全、读写锁结合起来实现一个工业级可用的配置管理类。class ThreadSafeConfigManager { public: static ThreadSafeConfigManager GetInstance() { static ThreadSafeConfigManager instance; return instance; } // 获取配置读多 std::optionalstd::string Get(const std::string key) const { std::shared_lock lock(rw_mutex_); auto it config_map_.find(key); if (it ! config_map_.end()) { return it-second; } return std::nullopt; } // 批量获取减少锁粒度 std::unordered_mapstd::string, std::string GetBatch(const std::vectorstd::string keys) const { std::shared_lock lock(rw_mutex_); std::unordered_mapstd::string, std::string result; for (const auto key : keys) { auto it config_map_.find(key); if (it ! config_map_.end()) { result[key] it-second; } } return result; } // 更新或设置配置写少 void Set(const std::string key, const std::string value) { std::unique_lock lock(rw_mutex_); config_map_[key] value; // 这里可以触发回调通知观察者 } // 从文件加载配置全量写 bool LoadFromFile(const std::string filepath) { // 解析文件... auto new_config parseConfigFile(filepath); if (!new_config) return false; { std::unique_lock lock(rw_mutex_); config_map_.swap(*new_config); // 使用swap避免拷贝且更新是原子的 } return true; } private: ThreadSafeConfigManager() default; ~ThreadSafeConfigManager() default; ThreadSafeConfigManager(const ThreadSafeConfigManager) delete; ThreadSafeConfigManager operator(const ThreadSafeConfigManager) delete; mutable std::shared_mutex rw_mutex_; std::unordered_mapstd::string, std::string config_map_; };这个类展示了几个最佳实践单例使用Magic Static实现。容器安全使用std::shared_mutex保护unordered_map。接口设计Get返回std::optional清晰表达“可能有可能无”。GetBatch一次获取多个键值减少锁的获取/释放次数。写操作优化LoadFromFile中使用swap来替换整个map这是一个原子操作在锁内比逐个插入更高效并且保证了配置切换的瞬间一致性。6. 常见问题排查与性能调优实录即使理解了所有原理实际编码和运行时还是会遇到各种问题。这里记录几个我踩过的坑和对应的排查技巧。6.1 死锁的预防与诊断死锁通常发生在需要多个锁的场景。经典条件是互斥、持有并等待、不可剥夺、循环等待。预防策略固定顺序上锁所有线程都按相同的全局顺序如锁的地址大小获取锁。// 假设有锁A和锁B void fixed_order(std::mutex mtx1, std::mutex mtx2) { // 确保先锁地址小的那个 auto first std::min(mtx1, mtx2, [](auto a, auto b){ return a b; }); auto second (mtx1 first) ? mtx2 : mtx1; std::lock_guardstd::mutex lock1(*first); std::lock_guardstd::mutex lock2(*second); // ... }使用std::lock一次性锁定多个锁C标准库提供了std::lock它可以一次性锁定两个或更多个互斥量且避免了死锁。std::mutex mtx1, mtx2; { std::unique_lockstd::mutex lock1(mtx1, std::defer_lock); std::unique_lockstd::mutex lock2(mtx2, std::defer_lock); std::lock(lock1, lock2); // 一次性锁定顺序由内部算法决定 // 临界区 }避免在持有锁时调用未知代码特别是回调函数或虚函数因为你不知道它内部会不会再去获取别的锁。诊断工具GDB当程序卡死时用gdb -p pid附加然后thread apply all bt查看所有线程的调用栈。死锁的线程通常会停在__lll_lock_wait或类似的锁等待函数上。HelgrindValgrind的线程错误检测工具。运行valgrind --toolhelgrind ./your_program它能检测出数据竞争、死锁的潜在风险。它对性能影响很大仅用于测试环境。Clang ThreadSanitizer (TSan)编译时添加-fsanitizethread运行时能检测数据竞争和死锁。比Helgrind快是线上测试的常用选择。6.2 性能瓶颈分析与锁争用优化当并发程序性能上不去时锁争用往往是罪魁祸首。可以使用perf或vtune进行性能剖析。perf锁分析示例# 记录性能事件 perf record -g -p pid -- sleep 10 # 查看报告关注mutex相关的等待事件 perf report # 或者使用perf lock子命令分析锁争用 perf lock record -p pid -- sleep 5 perf lock reportperf lock report会输出锁的等待时间、争用情况帮你找到“热点锁”。优化策略缩小临界区只把必须同步的代码放在锁内。例如耗时的I/O操作、复杂计算尽量移到锁外。锁粒度拆分一把大锁保护所有数据不如几把小锁分别保护不同的数据段例如一个哈希表可以用一个锁数组每个锁保护一个桶。使用读写锁如前所述读多写少场景的利器。无锁数据结构对于极端性能要求的场景考虑使用无锁队列、无锁哈希表。乐观锁与CAS对于冲突较少的场景可以使用“比较并交换”Compare-And-Swap的乐观并发控制。例如更新一个计数器时先读取旧值计算新值然后用atomic_compare_exchange_strong尝试更新如果失败被其他线程改了就重试。std::atomicint counter{0}; void optimistic_increment() { int old_value counter.load(std::memory_order_relaxed); while (!counter.compare_exchange_weak(old_value, old_value 1, std::memory_order_release, std::memory_order_relaxed)) { // 如果失败old_value已被更新为当前值循环重试 } }6.3 内存序与原子操作的深水区当你开始使用std::atomic和无锁编程时一定会遇到memory_order。这是C并发中最复杂的概念之一。简单来说它定义了原子操作周围非原子内存访问的可见性顺序。memory_order_seq_cst顺序一致性默认选项最强约束。保证所有线程看到的操作顺序一致。性能开销最大但最不容易出错。除非你有充分理由否则一直用它。memory_order_acquire/release配对使用用于实现“同步”关系。acquire读操作之后的读/写能看到release写操作之前的所有写。常用于互斥锁、自旋锁的实现。memory_order_relaxed最弱约束只保证原子操作本身的原子性不提供任何顺序保证。只适用于计数器等不需要同步其他内存的场景。一个忠告除非你在编写底层并发库并且有严格的性能证据表明seq_cst是瓶颈否则不要轻易使用relaxed或复杂的acquire/release组合。错误的记忆序会导致极其隐蔽的、难以重现的bug。我曾在一次优化中将一个计数器的seq_cst改为relaxed虽然性能提升了几个百分点但在一个边缘路径下由于缺少必要的同步导致状态机紊乱花了整整两天才定位到问题。在原子操作的世界里正确性永远优先于性能。线程安全是一个庞大而深邃的话题从单例模式到无锁编程每一层都有其精妙与陷阱。我的经验是在大多数应用开发中掌握好互斥锁、读写锁、条件变量配合RAII和良好的设计如缩小临界区、拆分锁粒度就能解决95%的并发问题。对于剩下的5%再考虑更高级的工具和技术。永远记住最简单的、能正确工作的方案就是最好的方案。在追求性能之前先用工具如TSan、Helgrind证明你的程序是正确的。

相关新闻