如何使线程安全类的构造函数与类其他部分同步?
线程安全类Blah的构造同步问题解决方案
问题背景
假设我定义了一个声称线程安全的类Blah:
class Blah { public: Blah () { m_blah = 123; } int blah () { std::lock_guard _{m_mutex}; m_blah += 1; return m_blah; } private: std::mutex m_mutex; int m_blah; };
当其他线程通过无同步方式(比如用std::memory_order_relaxed存储对象指针)访问该类实例时,构造函数中m_blah的初始化操作与成员函数的临界区会发生数据竞争——这会导致类的线程安全承诺失效,用户因此追责。
我有三种应对选择:
- 无需在意,认为是用户使用不当(多数情况下仍会称该类线程安全)
- 在构造函数中加锁同步内存(但可能因mutex状态未同步而失效)
- 通过其他方式在类内部同步内存
如果选择方案3,最优的实现方式是什么?
最优实现方案(方案3)
要解决构造与成员函数间的数据竞争,核心是确保构造函数对m_blah的初始化操作能对后续访问该对象的线程可见,同时避免直接在构造函数中使用未完全初始化的mutex(因为mutex自身的构造也需要保证可见性)。
推荐方案:用std::atomic保障初始化可见性
将m_blah声明为std::atomic<int>,同时保留成员函数中的mutex来保护m_blah += 1这类复合操作(如果仅需保护自增,也可以直接用atomic的原子操作替代mutex,视具体需求而定)。
修改后的代码如下:
#include <atomic> #include <mutex> class Blah { public: Blah () : m_blah(123) {} // atomic初始化自带可见性保障 int blah () { std::lock_guard _{m_mutex}; // 若仅需自增,可替换为return m_blah.fetch_add(1) + 1; 此时可去掉mutex int current = m_blah.load(std::memory_order_relaxed); m_blah.store(current + 1, std::memory_order_relaxed); return current + 1; } private: std::mutex m_mutex; // 若只有m_blah自增操作,可替换为atomic原子操作 std::atomic<int> m_blah; };
原理说明:
std::atomic的构造初始化会隐含内存屏障,保证初始化操作对所有后续访问该对象的线程可见,彻底消除构造与成员函数间的数据竞争。- 如果类中仅需保护
m_blah的自增这类原子操作,直接用std::atomic<int>的fetch_add即可,既保证线程安全,又避免锁的开销。
备选方案:静态初始化+工厂函数(适用于受控创建场景)
如果对象的创建是受控的(比如单例),可以利用C++11及以后的静态变量初始化线程安全性,确保对象完全构造后才被其他线程访问:
class Blah { private: Blah () : m_blah(123) {} // 私有构造,限制直接创建 public: static Blah& getInstance() { static Blah instance; // C++11+保证静态变量初始化线程安全 return instance; } int blah () { std::lock_guard _{m_mutex}; m_blah += 1; return m_blah; } private: std::mutex m_mutex; int m_blah; };
这种方式下,对象的构造由标准库保证线程安全,其他线程只能通过getInstance()获取完全初始化的对象,从根源上避免了无同步访问未构造完成对象的问题。
方案对比
- atomic方案:通用性强,适用于所有对象创建场景,无需限制对象创建方式,仅修改成员变量类型即可解决问题。
- 静态初始化方案:仅适用于单例或受控创建的对象,优点是无需修改成员变量,利用标准特性保证线程安全,但灵活性较差。
内容的提问来源于stack exchange,提问作者jeremiah
相关产品推荐
相关产品推荐

