单例中静态声明的区别

Differences between static declarations in singletons

(代码取自书籍:http://gameprogrammingpatterns.com/ by Robert Nystrom)

作者在上面的书中给出了两种不同的单例制作方法class:

第一个:

class FileSystem
{
public:
  static FileSystem& instance()
  {
    // Lazy initialize.
    if (instance_ == NULL) instance_ = new FileSystem();
    return *instance_;
  }

private:
  FileSystem() {}

  static FileSystem* instance_;
};

第二个:

class FileSystem
{
public:
  static FileSystem& instance()
  {
    static FileSystem *instance = new FileSystem();
    return *instance;
  }

private:
  FileSystem() {}
};

后来他说第二种方法更合适,因为它是线程安全的,而第一种不是。
是什么让第二个线程安全?
这两者在静态声明上有什么区别?

在前一种情况下,如果两个线程尝试同时创建实例,则可能会创建 2 个(或更多)单例对象副本。 (如果 **instance_** 被两者观察到 NULL 并且都创建一个 new 实例)。 (更糟糕的是,创建第一个实例的线程可能会在后续调用中获得不同的实例值)

而第二个使用 static 初始化并在第一次调用该函数时构造对象。因此,编译器保证 static FileSystem *instance = new FileSystem(); 在程序 single 的生命周期内最多执行一次,因此 atmist 对象的单个副本将随时存在。

这使得后来的设计线程对于 C++11 和更高版本的 C++ 编译器是安全的。尽管该设计在 C++03 和 C++98 实现中可能不安全。


以前设计的另一个缺点是,对象不能被销毁,而在后来的设计中,可以通过将typeof instance_更改为static FileSystem来销毁它。即

static FileSystem& instance()
{
  static FileSystem instance;
  return instance;
}

相关:Is Meyers implementation of Singleton pattern thread safe?

在第一个代码片段中,将 instance_ 指针设置为单例是一个 赋值 。它没有得到编译器的任何特殊处理。特别是,如果从并发线程调用 instance(),则可以执行多次。

当两个并发线程尝试评估 instance_ == NULL 并获得 true 时,这会产生问题。此时两个线程都创建了一个新实例,并将其分配给 instance_ 变量。分配给 instance_ 的第一个指针已泄漏,因为第二个线程立即覆盖它,导致对象无法访问。

在第二个代码片段中设置 instance 指针是 初始化 。编译器保证静态变量的初始化最多完成一次,而不管同时调用 instance() 的线程数是多少。本质上,系统中最多有一个单例的保证是由编译器提供的,没有任何用于处理并发的显式代码。

第一个版本不是线程安全的,因为多个线程可能会尝试在没有任何同步的情况下同时读取和修改 instance_,这会导致竞争条件。

从 C++11 开始,第二个版本是线程安全的。引自 cppreference静态局部变量 部分):

If multiple threads attempt to initialize the same static local variable concurrently, the initialization occurs exactly once (similar behavior can be obtained for arbitrary functions with std::call_once)

有了这个保证,对instance的修改只发生一次,并发读取没有问题。

不过,第二个版本在 C++11 之前不是线程安全的。