简体   繁体   English

便携式线程安全懒惰单身人士

[英]Portable thread-safe lazy singleton

Greetings to all. 祝福大家。

I'm trying to write a thread safe lazy singleton for future use. 我正在尝试编写一个线程安全的懒惰单例以备将来使用。 Here's the best I could come up with. 这是我能想到的最好的。 Can anyone spot any problems with it? 有人能发现任何问题吗? The key assumption is that static initialization occurs in a single thread before dynamic initialisations. 关键假设是静态初始化在动态初始化之前发生在单个线程中。 (this will be used for a commercial project and company is not using boost :(, life would be a breeze otherwise :) (这将用于商业项目,公司不使用提升:(,生活将是轻而易举的:)

PS: Haven't check that this compiles yet, my apologies. PS:没有检查这个编译,我的道歉。

/*

There are two difficulties when implementing the singleton pattern:

Problem (a):  The "global variable instantiation fiasco". TODO: URL
This is due to the unspecified order in which global variables are initialised. Static class members are equivalent
to a global variable in C++ during initialisation.

Problem (b):  Multi-threading.
Care must be taken to ensure that the mutex initialisation is handled properly with respect to problem (a).

*/


/*
Things achieved, maybe:

*) Portable

*) Lazy creation.

*) Safe from unspecified order of global variable initialisation.

*) Thread-safe.

*) Mutex is properly initialise when invoked during global variable intialisation:

*) Effectively lock free in instance().


*/



/************************************************************************************

Platform dependent mutex implementation

*/
class Mutex
{
public:
 void lock();
 void unlock();
};



/************************************************************************************

Threadsafe singleton 

*/
class Singleton
{
public:  // Interface
 static Singleton* Instance();


private:  // Static helper functions

 static Mutex* getMutex();


private:  // Static members

 static Singleton* _pInstance;

 static Mutex* _pMutex;


private:  // Instance members

 bool* _pInstanceCreated;  // This is here to convince myself that the compiler is not re-ordering instructions.


private:  // Singletons can't be coppied

 explicit Singleton();
 ~Singleton() { }
};


/************************************************************************************

We can't use a static class member variable to initialised the mutex due to the unspecified
order of initialisation of global variables.

Calling this from 

*/
Mutex* Singleton::getMutex()
{
 static Mutex* pMutex = 0;  // alternatively:  static Mutex* pMutex = new Mutex();
 if( !pMutex )
 { 
  pMutex = new Mutex();  // Constructor initialises the mutex: eg. pthread_mutex_init( ... )
 }

 return pMutex;
}


/************************************************************************************

This static member variable ensures that we call Singleton::getMutex() at least once before
the main entry point of the program so that the mutex is always initialised before any threads
are created.

*/
Mutex* Singleton::_pMutex = Singleton::getMutex();


/************************************************************************************
Keep track of the singleton object for possible deletion.

*/
Singleton* Singleton::_pInstance = Singleton::Instance();


/************************************************************************************
Read the comments in Singleton::Instance().

*/
Singleton::Singleton( bool* pInstanceCreated )
{
 fprintf( stderr, "Constructor\n" );

 _pInstanceCreated = pInstanceCreated; 
}


/************************************************************************************
Read the comments in Singleton::Instance().

*/
void Singleton::setInstanceCreated()
{
 _pInstanceCreated = true;
}


/************************************************************************************

Fingers crossed.

*/
Singleton* Singleton::Instance()
{
 /*

 'instance' is initialised to zero the first time control flows over it. So
 avoids the unspecified order of global variable initialisation problem.

 */ 
 static Singleton* instance = 0;


 /*
 When we do:

  instance = new Singleton( instanceCreated );

 the compiler can reorder instructions and any way it wants as long
 as the observed behaviour is consistent to that of a single threaded environment ( assuming
 that no thread-safe compiler flags are specified). The following is thus not threadsafe:

 if( !instance )
 {
  lock();
  if( !instance )
  {
   instance = new Singleton( instanceCreated );
  }
  lock();
 }

 Instead we use:

  static bool instanceCreated = false;

 as the initialisation indicator.
 */
 static bool instanceCreated = false;


 /*

 Double check pattern with a slight swist.

 */
 if( !instanceCreated )
 {
  getMutex()->lock();
  if( !instanceCreated )
  {
   /*
   The ctor keeps a persistent reference to 'instanceCreated'.

   In order to convince our-selves of the correct order of initialisation (I think
   this is quite unecessary
   */
   instance = new Singleton( instanceCreated );

   /*
   Set the reference to 'instanceCreated' to true.

   Note that since setInstanceCreated() actually uses the non-static
   member variable: '_pInstanceCreated', I can't see the compiler taking the
   liberty to call Singleton's ctor AFTER the following call. (I don't know
   much about compiler optimisation, but I doubt that it will break up the ctor into
   two functions and call one part of it before the following call and the other part after.
   */
   instance->setInstanceCreated();

   /*
   The double check pattern should now work.
   */
  }  
  getMutex()->unlock();
 }

 return instance;
}

No, this will not work. 不,这不行。 It is broken. 它被打破。

The problem has little/nothing to do with the compiler. 这个问题与编译器几乎没有任何关系。 It has to do with the order in which a second CPU will 'see' what the first CPU has done to memory. 它与第二个CPU“看到”第一个CPU对内存的作用的顺序有关。 The memory (and caches) will be consistent, but the timing of WHEN each CPU decides to write or read each part of memory/cache is indeterminate . 内存(和高速缓存)将保持一致,但每个CPU决定写入或读取内存/高速缓存的每个部分的时间是不确定的

So for CPU1: 所以对于CPU1:

instance = new Singleton( instanceCreated );
instance->setInstanceCreated();

Let's consider the compiler first. 我们先考虑编译器。 There is NO reason why the compiler doesn't reorder or otherwise alter these functions. 编译器没有理由不重新排序或以其他方式更改这些功能。 Maybe like: 也许喜欢:

temp_register = new Singleton(instanceCreated);
temp_register->setInstanceCreated();
instance = temp_register;

or many other possibilities - like you said as long as single-threaded observed behaviour is consistent. 或者许多其他可能性 - 就像你说的那样,只要单线程观察到的行为是一致的。 This DOES include things like " break up the ctor into two functions and call one part of it before the following call and the other part after." 这包括诸如“将ctor分解成两个函数并在下一个调用之前调用其中一部分而后调用另一部分”之类的内容。

Now, it probably wouldn't break it up into 2 calls, but it would INLINE the ctor, particularly since it is so small. 现在,它可能不会将其分解为2个调用,但它会使ctor内联,特别是因为它非常小。 Then, once inlined, everything may be reordered, as if the ctor was broken in 2, for example. 然后,一旦内联,一切都可能被重新排序,就好像ctor在2中被破坏一样。

In general, I would say not only is it possible that the compiler reordered things, it is probable - ie for the code you have, there is probably a reordering (once inlined, and inlining is likely) that is 'better' than the order given by the C++ code. 一般来说,我不仅会说编译器有可能重新排序,很可能 - 也就是说,对于你所拥有的代码,可能有一个比订单“更好”的重新排序(一次内联,内联很可能)由C ++代码给出。

But let's leave that aside, and try to understand the real issues of double-checked locking. 但是让我们把它放在一边,并尝试理解双重检查锁定的真正问题。 So, let's just assume the compiler didn't reorder anything. 所以,让我们假设编译器没有重新排序任何东西。 What about the CPU? CPU怎么样? Or more importantly CPU s - plural. 或者更重要的是CPU s - 复数。

The first CPU, 'CPU1' needs to follow the instructions given by the compiler, in particular, it needs to write to memory the things it has been told to write: 第一个CPU,'CPU1'需要遵循编译器给出的指令,特别是,它需要向内存写入已被告知写入的内容:

  • instance , instance
  • instanceCreated
  • other member variable of the Singleton (ie your Singleton does DO something, and has some state, doesn't it?) Singleton的其他成员变量(即你的Singleton做了什么,有一些状态,不是吗?)

Actually, that 'other member variable' stuff is really important. 实际上,“其他成员变量”的东西非常重要。 Important for your singleton - that's its real purpose right?, and important for our discussion. 对你的单身人士来说很重要 - 这才是真正的目的吗?对我们的讨论很重要。 So let's give it a name: important_data . 所以让我们给它一个名字: important_data ie instance->important_data . instance->important_data And maybe instance->important_function() , which uses important_data . 也许是instance->important_function() ,它使用了important_data Etc. 等等。

As mentioned, let's assume the compiler has written the code such that these items are written in the order you are expecting, namely: 如上所述,让我们假设编译器编写了代码,使这些项按您期望的顺序编写,即:

  1. important_data - written inside the ctor, called from important_data - 写在ctor里面,来自

    instance = new Singleton(instanceCreated);

  2. instance - assigned right after new/ctor returns instance - 在new / ctor返回后立即分配

  3. instanceCreated - inside setInstanceCreated() instanceCreated - 在setInstanceCreated()内

Now, the CPU hands these writes off to the memory bus. 现在,CPU将这些写操作交给内存总线。 Know what the memory bus does? 知道内存总线的作用吗? IT REORDERS THEM. 它回归他们。 The CPU and architecture has the same constraints as the compiler - ie make sure this one CPU sees things consistently - ie single threaded consistent. CPU和体系结构具有与编译器相同的约束 - 即确保这一个CPU始终看到事物 - 即单线程一致。 So if, for example, instance and instanceCreated are on the same cache-line (highly likely, actually), they might be written together, and since they were just read, that cache-line is 'hot', so maybe they get written FIRST before important_data , so that that cache-line can be retired to make room for the cache-line where important_data lives. 因此,例如,如果instanceinstanceCreated在同一个缓存行上(很可能,实际上),它们可能会被一起写入,并且由于它们只是被读取,因此缓存行是“热门的”,所以它们可能会被写入首先在important_data之前,以便可以停用该缓存行,以便为important_data所在的缓存行腾出空间。

Did you see that? 你看见了吗? instanceCreated and instance were just committed to memory BEFORE important_data . instanceCreatedinstance刚刚提交到了important_data之前的内存。 Note that CPU1 doesn't care, because it is living in a single-threaded world... 请注意,CPU1并不关心,因为它生活在一个单线程的世界......

So now introduce CPU2: 所以现在介绍CPU2:

CPU2 comes in, sees instanceCreated == true and instance != NULL and thus goes off and decides to call Singleton::Instance()->important_function(), which uses important_data , which is uninitialized. CPU2进来,看到instanceCreated == trueinstance != NULL因此关闭并决定调用Singleton :: Instance() - > important_function(),它使用未初始化的important_data CRASH BANG BOOM. CRASH BANG BOOM。

By the way, it gets worse. 顺便说一句,它变得更糟。 So far, we've seen that the compiler could reorder, but we're pretending it didn't. 到目前为止,我们已经看到编译器可以重新排序,但我们假装它没有。 Let's go one step further and pretend that CPU1 did NOT reorder any of the memory writing. 让我们更进一步,假装CPU1没有重新排序任何内存写入。 Are we OK now? 我们现在好吗?

No. Of course not. 不,当然不。

Just as CPU1 decided to optimize/reorder its memory writes, CPU2 can REORDER ITS READS ! 就像CPU1决定优化/重新排序其内存写入一样,CPU2可以重新读取它

CPU2 comes in and sees CPU2进来看到了

if (!instanceCreated) ...

so it needs to read instanceCreated . 所以它需要读取instanceCreated Ever heard of 'speculative execution'? 有没有听说过“投机执行”? (Great name for a FPS game, by the way). (顺便说一下,FPS游戏的好名字)。 If the memory bus isn't busy doing anything, CPU2 might pre-read some other values 'hoping' that instanceCreated is true. 如果内存总线没有忙于做任何事情,CPU2可能会预读一些其他值'希望'instanceCreated为真。 ie it may pre-read important_data for example. 也就是说它可以预先读取important_data Maybe important_data (or the uninitialized, possibly re-claimed-by-the-allocator memory that will become important_data ) is already in CPU2's cache. 也许important_data (或未初始化的,可能重新声明的将成为important_data的分配器内存)已经在CPU2的缓存中。 Or maybe (more likely?) CPU2 just free'd that memory, and the allocator wrote NULL in its first 4 bytes (allocators often use that memory for their free-lists), so actually, the memory soon-to-become important_data may actually still be in the write queue of CPU2 . 或者(更有可能?)CPU2刚刚释放了那个内存,并且分配器在其前4个字节中写入了NULL(分配器经常将该内存用于它们的空闲列表),所以实际上,内存很快就会成为important_data实际上仍然在CPU2的写队列中 In that case, why would CPU2 bother re-reading that memory, when it hasn't even finished writing it yet!? 在那种情况下,为什么CPU2会重新读取那个内存,当它还没有写完它时呢!? (it wouldn't - it would just get the values from its write-queue.) (它不会 - 它只会从其写入队列中获取值。)

Did that make sense? 这有意义吗? If not, imagine that the value of instance (which is a pointer) is 0x17e823d0. 如果没有,想象instance (它是一个指针)的值是0x17e823d0。 What was that memory doing before it became (becomes) the Singleton? 在成为(变成)单身人士之前,那段记忆是做什么的? Is that memory still in the write-queue of CPU2?... 那个内存还在CPU2的写队列中吗?...

Or basically, don't even think about why it might want to do so, but realize that CPU2 might read important_data first, then instanceCreated second. 或者基本上,甚至不考虑它为什么要这样做,但是要意识到CPU2可能首先读取important_data ,然后读取instanceCreated So even though CPU1 may have wrote them in order CPU2 sees 'crap' in important_data , then sees true in instanceCreated (and who knows what in instance !). 因此, 即使CPU1可能已经按照 CPU2在important_data看到'crap'的顺序编写它们 ,然后在instanceCreated看到true (并且谁知道instance !)。 Again, CRASH BANG BOOM. 再次,CRASH BANG BOOM。 Or BOOM CRASH BANG, since by now you realize that the order isn't guaranteed... 或者BOOM CRASH BANG,因为到现在为止你意识到订单不能保证......

It's usually better to have a non-lazy singleton which does nothing in its constructor, and then in GetInstance do a thread-safe call once to a function which allocates any expensive resources. 拥有一个在构造函数中什么也不做的非惰性单例通常会更好,然后在GetInstance中对一个分配任何昂贵资源的函数执行一次线程安全调用。 You're already creating a Mutex non-lazily, so why not just put the mutex and some kind of Pimpl in your Singleton object? 你已经非懒惰地创建了一个Mutex了,为什么不在你的Singleton对象中放入互斥量和某种Pimpl呢?

By the way, this is easier on Posix: 顺便说一句,这在Posix上更容易:

struct Singleton {
    static Singleton *GetInstance() {
        pthread_once(&control, doInit);
        return instance;
    }

private:
    static void doInit() {
        // slight problem: we can't throw from here, or fail
        try {
            instance = new Singleton();
        } catch (...) {
            // we could stash an error indicator in a static member,
            // and check it in GetInstance.
            std::abort();
        }
    }

    static pthread_once_t control;
    static Singleton *instance;
};

pthread_once_t Singleton::control = PTHREAD_ONCE_INIT;
Singleton *Singleton::instance = 0;

There do exist pthread_once implementations for Windows and other platforms. 对于Windows和其他平台,确实存在pthread_once实现。

If you wish to see an in-depth discussion of Singletons, the various policies about their lifetime and the thread safety issues, I can only recommend a good read: " Modern C++ Design " by Alexandrescu. 如果您希望看到有关Singletons的深入讨论,关于其生命周期和线程安全问题的各种政策,我只能推荐一个很好的阅读:Alexandrescu的“ 现代C ++设计 ”。

The implementation is presented on the web in Loki, find it here ! 实施在Loki网站上呈现,在这里找到!

And yes, it does hold in a single header file. 是的,它确实存在于单个头文件中。 So I would really encourage you to at least grab the file and read it, and better yet read the book to have the full-blown reflection. 所以我真的鼓励你至少抓住文件并阅读它,最好还是阅读这本书以获得全面的反思。

At global scope in your code: 在代码的全局范围内:

/************************************************************************************
Keep track of the singleton object for possible deletion.

*/
Singleton* Singleton::_pInstance = Singleton::Instance();

This makes your implementation not lazy. 这使您的实现不是懒惰的。 Presumably you want to set _pInstance to NULL at global scope, and assign to it after you construct the singleton inside Instance() before you unlock the mutex. 大概你想在全局范围内将_pInstance设置为NULL,并在解锁互斥锁之前在Instance()中构造单例之后分配给它。

More food for thought from Meyers & Alexandrescu, with Singleton being the specific target: C++ and the Perils of Double-Checked Locking . Meyers&Alexandrescu提供了更多值得思考的东西,Singleton是特定的目标: C ++和双重锁定的危险 It's a bit of a prickly problem. 这有点棘手的问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM