C ++中单例的线程安全延迟构造

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6915/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-27 12:12:41  来源:igfitidea点击:

Thread safe lazy construction of a singleton in C++

c++multithreadingsingletonconstructionlazy-initialization

提问by pauldoo

Is there a way to implement a singleton object in C++ that is:

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

  1. Lazily constructed in a thread safe manner (two threads might simultaneously be the first user of the singleton - it should still only be constructed once).
  2. Doesn't rely on static variables being constructed beforehand (so the singleton object is itself safe to use during the construction of static variables).
  1. 以线程安全的方式懒惰地构造(两个线程可能同时是单例的第一个用户 - 它仍然应该只构造一次)。
  2. 不依赖于预先构造的静态变量(因此在构造静态变量期间使用单例对象本身是安全的)。

(I don't know my C++ well enough, but is it the case that integral and constant static variables are initialized before any code is executed (ie, even before static constructors are executed - their values may already be "initialized" in the program image)? If so - perhaps this can be exploited to implement a singleton mutex - which can in turn be used to guard the creation of the real singleton..)

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



Excellent, it seems that I have a couple of good answers now (shame I can't mark 2 or 3 as being the answer). There appears to be two broad solutions:

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

  1. Use static initialisation (as opposed to dynamic initialisation) of a POD static variable, and implementing my own mutex with that using the builtin atomic instructions. This was the type of solution I was hinting at in my question, and I believe I knew already.
  2. Use some other library function like pthread_onceor boost::call_once. These I certainly didn't know about - and am very grateful for the answers posted.
  1. 使用 POD 静态变量的静态初始化(而不是动态初始化),并使用内置原子指令实现我自己的互斥锁。这是我在问题中暗示的解决方案类型,我相信我已经知道了。
  2. 使用其他一些库函数,如pthread_onceboost::call_once。这些我当然不知道 - 非常感谢发布的答案。

采纳答案by Derek Park

Basically, you're asking for synchronized creation of a singleton, without using any synchronization (previously-constructed variables). In general, no, this is not possible. You need something available for synchronization.

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

As for your other question, yes, static variables which can be statically initialized (i.e. no runtime code necessary) are guaranteed to be initialized before other code is executed. This makes it possible to use a statically-initialized mutex to synchronize creation of the singleton.

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

From the 2003 revision of the C++ standard:

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

Objects with static storage duration (3.7.1) shall be zero-initialized (8.5) before any other initialization takes place. Zero-initialization and initialization with a constant expression are collectively called static initialization; all other initialization is dynamic initialization. Objects of POD types (3.9) with static storage duration initialized with constant expressions (5.19) shall be initialized before any dynamic initialization takes place. Objects with static storage duration defined in namespace scope in the same translation unit and dynamically initialized shall be initialized in the order in which their definition appears in the translation unit.

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

If you knowthat you will be using this singleton during the initialization of other static objects, I think you'll find that synchronization is a non-issue. To the best of my knowledge, all major compilers initialize static objects in a single thread, so thread-safety during static initialization. You can declare your singleton pointer to be NULL, and then check to see if it's been initialized before you use it.

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

However, this assumes that you knowthat you'll use this singleton during static initialization. This is also not guaranteed by the standard, so if you want to be completely safe, use a statically-initialized mutex.

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

Edit: Chris's suggestion to use an atomic compare-and-swap would certainly work. If portability is not an issue (and creating additional temporary singletons is not a problem), then it is a slightly lower overhead solution.

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

回答by Chris Hanson

Unfortunately, Matt's answer features what's called double-checked lockingwhich isn't supported by the C/C++ memory model. (It is supported by the Java 1.5 and later — and I think .NET — memory model.) This means that between the time when the pObj == NULLcheck takes place and when the lock (mutex) is acquired, pObjmay have already been assigned on another thread. Thread switching happens whenever the OS wants it to, not between "lines" of a program (which have no meaning post-compilation in most languages).

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

Furthermore, as Matt acknowledges, he uses an intas a lock rather than an OS primitive. Don't do that. Proper locks require the use of memory barrier instructions, potentially cache-line flushes, and so on; use your operating system's primitives for locking. This is especially important because the primitives used can change between the individual CPU lines that your operating system runs on; what works on a CPU Foo might not work on CPU Foo2. Most operating systems either natively support POSIX threads (pthreads) or offer them as a wrapper for the OS threading package, so it's often best to illustrate examples using them.

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

If your operating system offers appropriate primitives, and if you absolutely need it for performance, instead of doing this type of locking/initialization you can use an atomic compare and swapoperation to initialize a shared global variable. Essentially, what you write will look like this:

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

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;
}

This only works if it's safe to create multiple instances of your singleton (one per thread that happens to invoke GetSingleton() simultaneously), and then throw extras away. The OSAtomicCompareAndSwapPtrBarrierfunction provided on Mac OS X — most operating systems provide a similar primitive — checks whether pObjis NULLand only actually sets it to tempto it if it is. This uses hardware support to really, literally only perform the swap onceand tell whether it happened.

这只适用于创建多个单例实例(每个线程一个同时调用 GetSingleton() 是安全的),然后扔掉额外的实例。OSAtomicCompareAndSwapPtrBarrierMac OS X 上提供的函数——大多数操作系统都提供类似的原语——检查是否pObjNULL,如果是,则仅将其设置temp为它。这使用硬件支持来真正地,字面上只执行一次交换并判断它是否发生了。

Another facility to leverage if your OS offers it that's in between these two extremes is pthread_once. This lets you set up a function that's run only once - basically by doing all of the locking/barrier/etc. trickery for you - no matter how many times it's invoked or on how many threads it's invoked.

如果您的操作系统提供介于这两个极端之间的另一个可利用的工具是pthread_once. 这使您可以设置一个仅运行一次的函数 - 基本上是通过执行所有锁定/屏障/等。对你来说是个诡计——不管它被调用了多少次或者它被调用了多少个线程。

回答by Frerich Raabe

Here's a very simple lazily constructed singleton getter:

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

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

This is lazy, and the next C++ standard (C++0x) requires it to be thread safe. In fact, I believe that at least g++ implements this in a thread safe manner. So if that's your target compiler orif you use a compiler which also implements this in a thread safe manner (maybe newer Visual Studio compilers do? I don't know), then this might be all you need.

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

Also see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.htmlon this topic.

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

回答by Chris Jester-Young

You can't do it without any static variables, however if you are willing to tolerate one, you can use Boost.Threadfor this purpose. Read the "one-time initialisation" section for more info.

如果没有任何静态变量,您就无法做到这一点,但是如果您愿意容忍一个,您可以为此目的使用Boost.Thread。阅读“一次性初始化”部分了解更多信息。

Then in your singleton accessor function, use boost::call_onceto construct the object, and return it.

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

回答by 0124816

For gcc, this is rather easy:

对于 gcc,这很容易:

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

GCC will make sure that the initialization is atomic. For VC++, this is not the case. :-(

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

One major issue with this mechanism is the lack of testability: if you need to reset the LazyType to a new one between tests, or want to change the LazyType* to a MockLazyType*, you won't be able to. Given this, it's usually best to use a static mutex + static pointer.

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

Also, possibly an aside: It's best to always avoid static non-POD types. (Pointers to PODs are OK.) The reasons for this are many: as you mention, initialization order isn't defined -- neither is the order in which destructors are called though. Because of this, programs will end up crashing when they try to exit; often not a big deal, but sometimes a showstopper when the profiler you are trying to use requires a clean exit.

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

回答by n-alexander

  1. read on weak memory model. It can break double-checked locks and spinlocks. Intel is strong memory model (yet), so on Intel it's easier

  2. carefully use "volatile" to avoid caching of parts the object in registers, otherwise you'll have initialized the object pointer, but not the object itself, and the other thread will crash

  3. the order of static variables initialization versus shared code loading is sometimes not trivial. I've seen cases when the code to destruct an object was already unloaded, so the program crashed on exit

  4. such objects are hard to destroy properly

  1. 阅读弱记忆模型。它可以破坏双重检查锁和自旋锁。英特尔是强大的内存模型(还),所以在英特尔上它更容易

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

  3. 静态变量初始化与共享代码加载的顺序有时并不简单。我见过销毁对象的代码已经卸载的情况,因此程序在退出时崩溃

  4. 这样的物体很难正确销毁

In general singletons are hard to do right and hard to debug. It's better to avoid them altogether.

一般来说,单例很难做对,也很难调试。最好完全避免它们。

回答by OJ.

While this question has already been answered, I think there are some other points to mention:

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

  • If you want lazy-instantiation of the singleton while using a pointer to a dynamically allocated instance, you'll have to make sure you clean it up at the right point.
  • You could use Matt's solution, but you'd need to use a proper mutex/critical section for locking, and by checking "pObj == NULL" both before and after the lock. Of course, pObjwould also have to be static;) . A mutex would be unnecessarily heavy in this case, you'd be better going with a critical section.
  • 如果您希望在使用指向动态分配实例的指针时对单例进行延迟实例化,则必须确保在正确的位置对其进行清理。
  • 您可以使用 Matt 的解决方案,但您需要使用适当的互斥锁/关键部分进行锁定,并在锁定前后检查“pObj == NULL”。当然,pObj也必须是静态的;) 。在这种情况下,互斥量会不必要地沉重,您最好使用临界区。

But as already stated, you can't guarantee threadsafe lazy-initialisation without using at least one synchronisation primitive.

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

Edit: Yup Derek, you're right. My bad. :)

编辑:是的,德里克,你说得对。我的错。:)

回答by Derek Park

You could use Matt's solution, but you'd need to use a proper mutex/critical section for locking, and by checking "pObj == NULL" both before and after the lock. Of course, pObj would also have to be static ;) . A mutex would be unnecessarily heavy in this case, you'd be better going with a critical section.

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

OJ, that doesn't work. As Chris pointed out, that's double-check locking, which is not guaranteed to work in the current C++ standard. See: C++ and the Perils of Double-Checked Locking

OJ,那行不通。正如 Chris 指出的那样,这是双重检查锁定,不能保证在当前的 C++ 标准中工作。请参阅:C++ 和双重检查锁定的危险

Edit: No problem, OJ. It's really nice in languages where it does work. I expect it will work in C++0x (though I'm not certain), because it's such a convenient idiom.

编辑:没问题,OJ。在它可以工作的语言中真的很好。我希望它能在 C++0x 中工作(虽然我不确定),因为它是如此方便的习惯用法。

回答by Mat Noguchi

I suppose saying don't do this because it's not safe and will probably break more often than just initializing this stuff in main()isn't going to be that popular.

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

(And yes, I know that suggesting that means you shouldn't attempt to do interesting stuff in constructors of global objects. That's the point.)

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