【问题标题】:Is implementation of double checked singleton thread-safe?双重检查单例线程的实现是否安全?
【发布时间】:2017-04-08 10:29:42
【问题描述】:

我知道线程安全单例的常见实现是这样的:

Singleton* Singleton::instance() {
   if (pInstance == 0) {
      Lock lock;
      if (pInstance == 0) {
         Singleton* temp = new Singleton; // initialize to temp
         pInstance = temp; // assign temp to pInstance
      }
   }
   return pInstance;
}

但是为什么他们说它是线程安全的实现呢?
例如,第一个线程可以通过pInstance == 0 上的两个测试,创建new Singleton 并将其分配给temp 指针,然后开始 分配pInstance = temp(据我所知,指针赋值操作不是原子的)。
同时,第二个线程测试第一个pInstance == 0,其中pInstance 只分配了一半。它不是 nullptr 但也不是有效指针,然后从函数返回。 这样的情况会发生吗?我在任何地方都没有找到答案,似乎这是一个非常正确的实现,我什么都不懂

【问题讨论】:

标签: c++ multithreading singleton double-checked-locking


【解决方案1】:

C++ 并发规则并不安全,因为pInstance 的第一次读取不受锁或类似东西的保护,因此无法与写入正确同步( 受保护)。因此存在数据竞争,因此存在未定义的行为。此 UB 的一个可能结果正是您所确定的:第一次检查读取的垃圾值 pInstance 只是由不同的线程写入。

常见的解释是,在更常见的情况下(pInstance 已经有效),它节省了获取锁(一个潜在的耗时操作)。但是,这并不安全。

由于 C++11 及更高版本保证函数范围静态变量的初始化只发生一次并且是线程安全的,因此在 C++ 中创建单例的最佳方法是在函数中使用静态局部变量:

Singleton& Singleton::instance() {
   static Singleton s;
   return s;
}

请注意,不需要动态分配或指针返回类型。


正如 cmets 中提到的Voo,上面假设pInstance 是一个原始指针。如果它是std::atomic<Singleton*>,则代码将按预期正常工作。当然,原子读取是否比获取锁慢得多是一个问题,这应该通过分析来回答。尽管如此,这将是一个相当没有意义的练习,因为静态局部变量在所有情况下都更好,但非常模糊。

【讨论】:

  • 如果 pInstance 被声明为 std::atomic 它是完全安全的,但仍然是一个不好的方法。
  • @Voo,您仍然需要注意 order 参数,否则您可能会看到 pInstance 的正确值,但看不到它指向的对象。
  • @AProgrammer 嗯?给定的代码没有指定任何顺序,所以使用顺序一致性,这怎么会造成麻烦?
  • 请注意,一些硬件/C++ 实现提供了对齐指针的原子读/写,并且静态指针是对齐的。但是,除非您得到编译器的明确保证,否则您不能信任它的更高版本。
  • 如果其他具有静态存储的对象需要访问其析构函数中的 Singleton(该其他对象可能是另一个 Singleton),则此解决方案存在潜在的破坏顺序问题。虽然根据我的经验,这不是一个非常常见的问题,但可以通过动态分配 Singleton 并且从不删除它来很容易地消除它,这保证了 Singleton 将与程序一样长。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-12
  • 1970-01-01
  • 1970-01-01
  • 2013-12-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多