【问题标题】:Thread safe lazy construction of a singleton in C++C++ 中单例的线程安全惰性构造
【发布时间】:2010-09-05 15:14:34
【问题描述】:

有没有办法在 C++ 中实现单例对象,即:

  1. 以线程安全的方式延迟构造(两个线程可能同时是单例的第一个用户 - 它仍然应该只构造一次)。
  2. 不依赖于预先构造的静态变量(因此在构造静态变量期间使用单例对象本身是安全的)。

(我不太了解我的 C++,但是在执行任何代码之前初始化整数和常量静态变量是否是这种情况(即,甚至在执行静态构造函数之前 - 它们的值可能已经“初始化”在程序图像中)?如果是这样 - 也许可以利用它来实现单例互斥锁 - 这又可以用来保护真正单例的创建..)


太好了,我现在似乎有几个很好的答案(遗憾的是我不能将 2 或 3 标记为 答案)。似乎有两种广泛的解决方案:

  1. 使用 POD 静态变量的静态初始化(与动态初始化相反),并使用内置原子指令实现我自己的互斥锁。这是我在问题中暗示的解决方案类型,我相信我已经知道了。
  2. 使用其他一些库函数,如pthread_onceboost::call_once。这些我当然不知道 - 非常感谢发布的答案。

【问题讨论】:

    标签: c++ multithreading singleton construction lazy-initialization


    【解决方案1】:

    不幸的是,Matt 的答案具有 C/C++ 内存模型不支持的所谓双重检查锁定。 (Java 1.5 及更高版本(我认为是 .NET)内存模型支持它。)这意味着在发生pObj == NULL 检查和获取锁(互斥锁)之间,pObj 可能有已经被分配到另一个线程。线程切换发生在操作系统想要的任何时候,而不是在程序的“行”之间(在大多数语言中,编译后没有意义)。

    此外,正如马特承认的那样,他使用int 作为锁而不是操作系统原语。不要那样做。正确的锁需要使用内存屏障指令、可能的缓存行刷新等;使用操作系统的原语进行锁定。这一点尤其重要,因为使用的原语可以在操作系统运行的各个 CPU 行之间发生变化;在 CPU Foo 上有效的东西可能在 CPU Foo2 上无效。大多数操作系统要么原生支持 POSIX 线程 (pthread),要么将它们作为 OS 线程包的包装器提供,因此通常最好使用它们来说明示例。

    如果您的操作系统提供了适当的原语,并且您绝对需要它来提高性能,那么您可以使用原子比较和交换操作来初始化共享全局,而不是执行这种类型的锁定/初始化多变的。从本质上讲,您编写的内容如下所示:

    MySingleton *MySingleton::GetSingleton() {
        if (pObj == NULL) {
            // create a temporary instance of the singleton
            MySingleton *temp = new MySingleton();
            if (OSAtomicCompareAndSwapPtrBarrier(NULL, temp, &pObj) == false) {
                // if the swap didn't take place, delete the temporary instance
                delete temp;
            }
        }
    
        return pObj;
    }
    

    这只有在创建多个单例实例是安全的情况下才有效(每个线程一个碰巧同时调用 GetSingleton()),然后扔掉额外的东西。 Mac OS X 上提供的OSAtomicCompareAndSwapPtrBarrier 函数(大多数操作系统提供类似的原语)检查pObj 是否为NULL,如果是,则仅将其实际设置为temp。这使用硬件支持,实际上只执行交换一次并判断它是否发生。

    如果您的操作系统提供介于这两个极端之间的另一个工具,则可以使用pthread_once。这使您可以设置仅运行一次的功能-基本上是通过执行所有锁定/障碍/等。对你来说是个骗局——不管它被调用了多少次,也不管它被调用了多少个线程。

    【讨论】:

      【解决方案2】:

      基本上,您要求同步创建单例,而不使用任何同步(以前构造的变量)。一般来说,不,这是不可能的。您需要一些可用于同步的东西。

      至于你的另一个问题,是的,可以静态初始化的静态变量(即不需要运行时代码)保证在执行其他代码之前被初始化。这使得使用静态初始化的互斥锁来同步创建单例成为可能。

      从 C++ 标准的 2003 年修订版开始:

      在任何其他初始化发生之前,具有静态存储持续时间 (3.7.1) 的对象应进行零初始化 (8.5)。零初始化和用常量表达式初始化统称为静态初始化;所有其他初始化都是动态初始化。使用常量表达式 (5.19) 初始化的具有静态存储持续时间的 POD 类型 (3.9) 的对象应在任何动态初始化发生之前进行初始化。在同一翻译单元的命名空间范围内定义的静态存储持续时间并动态初始化的对象应按照其定义在翻译单元中出现的顺序进行初始化。

      如果您知道您将在初始化其他静态对象期间使用此单例,我想您会发现同步不是问题。据我所知,所有主要编译器都在单个线程中初始化静态对象,因此在静态初始化期间是线程安全的。您可以将您的单例指针声明为 NULL,然后在使用它之前检查它是否已初始化。

      但是,这假设您知道您将在静态初始化期间使用此单例。标准也不能保证这一点,因此如果您想完全安全,请使用静态初始化的互斥锁。

      编辑:Chris 使用原子比较和交换的建议肯定会奏效。如果可移植性不是问题(并且创建额外的临时单例也不是问题),那么它是一个开销稍低的解决方案。

      【讨论】:

        【解决方案3】:

        你不能没有任何静态变量,但是如果你愿意容忍一个,你可以使用Boost.Thread来达到这个目的。阅读“一次性初始化”部分了解更多信息。

        然后在你的单例访问器函数中,使用boost::call_once 构造对象,并返回它。

        【讨论】:

        • 只是我的看法,但我认为您必须小心使用 Boost。即使它有很多与线程相关的子项目,我也不相信它的线程安全。 (这是在相隔几年执行两次审计之后,并看到错误报告以“无法修复”而关闭)。
        【解决方案4】:

        虽然已经回答了这个问题,但我认为还有其他几点需要提及:

        • 如果您希望在使用指向动态分配实例的指针时对单例进行延迟实例化,则必须确保在正确的位置清理它。
        • 您可以使用 Matt 的解决方案,但您需要使用适当的互斥体/临界区进行锁定,并在锁定前后检查“pObj == NULL”。当然,pObj 也必须是 static ;) . 在这种情况下,互斥体会不必要地繁重,最好使用关键部分。

        但如前所述,如果不使用至少一个同步原语,就无法保证线程安全的延迟初始化。

        编辑:是的,Derek,你是对的。我的错。 :)

        【讨论】:

          【解决方案5】:

          您可以使用 Matt 的解决方案,但您需要使用适当的互斥锁/临界区进行锁定,并在锁定前后检查“pObj == NULL”。当然, pObj 也必须是静态的;)。在这种情况下,互斥量会不必要地繁重,最好使用关键部分。

          OJ,这行不通。正如克里斯指出的那样,这是双重检查锁定,不能保证在当前的 C++ 标准中工作。见:C++ and the Perils of Double-Checked Locking

          编辑:没问题,OJ。它在它确实有效的语言中非常好。我希望它可以在 C++0x 中工作(虽然我不确定),因为它是一个非常方便的习语。

          【讨论】:

            【解决方案6】:

            我想说不要这样做,因为它不安全,并且可能会比仅仅在 main() 中初始化这些东西更频繁地崩溃不会那么受欢迎。

            (是的,我知道这意味着你不应该尝试在全局对象的构造函数中做有趣的事情。这就是重点。)

            【讨论】:

              【解决方案7】:

              对于 gcc,这相当简单:

              LazyType* GetMyLazyGlobal() {
                  static const LazyType* instance = new LazyType();
                  return instance;
              }
              

              GCC 将确保初始化是原子的。 对于 VC++,情况并非如此。 :-(

              这种机制的一个主要问题是缺乏可测试性:如果您需要在测试之间将 LazyType 重置为新的,或者想要将 LazyType* 更改为 MockLazyType*,您将无法做到。鉴于此,通常最好使用静态互斥体 + 静态指针。

              另外,可能还有一点:最好始终避免使用静态非 POD 类型。 (指向 POD 的指针是可以的。)这样做的原因有很多:正如您所提到的,初始化顺序没有定义——尽管调用析构函数的顺序也没有定义。因此,程序在尝试退出时最终会崩溃;通常没什么大不了的,但有时当您尝试使用的分析器需要一个干净的退出时,这会是一个阻碍。

              【讨论】:

              【解决方案8】:
              1. 读取弱内存模型。它可以打破双重检查锁和自旋锁。英特尔是强大的内存模型(但),所以在英特尔上更容易

              2. 小心使用“volatile”来避免缓存部分对象在寄存器中,否则你将初始化对象指针,而不是对象本身,另一个线程将崩溃

              3. 静态变量初始化与共享代码加载的顺序有时并不重要。我见过破坏对象的代码已经被卸载,所以程序在退出时崩溃的情况

              4. 此类物体很难正确销毁

              一般来说,单例很难正确处理,也很难调试。最好完全避免它们。

              【讨论】:

                【解决方案9】:

                这是一个非常简单的惰性构造的单例 getter:

                Singleton *Singleton::self() {
                    static Singleton instance;
                    return &instance;
                }
                

                这是惰性的,下一个 C++ 标准 (C++0x) 要求它是线程安全的。事实上,我相信至少 g++ 以线程安全的方式实现了这一点。因此,如果这是您的目标编译器,如果您使用的编译器也以线程安全的方式实现这一点(也许较新的 Visual Studio 编译器会这样做?我不知道),那么这可能就是您所需要的.

                另请参阅 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html 关于此主题。

                【讨论】:

                • 不错!这将比我们当前的解决方案整洁得多。 C++0x(或者应该是 C++1x)什么时候最终完成..?
                • VS2015 引入了对这种初始化模式的线程安全支持。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多