祝福大家。

我正在尝试编写线程安全的懒惰单例以供将来使用。这是我能想到的最好的方法。任何人都可以发现它的任何问题吗?关键假设是静态初始化在动态初始化之前在单个线程中发生。 (这将用于商业项目,而公司不使用boost :(否则,生活将变得轻而易举:)

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

最佳答案

不,这行不通。它被打破。

这个问题与编译器几乎没有关系。它与第二个CPU将“看到”第一个CPU对存储器所做的操作的顺序有关。内存(和缓存)将保持一致,但是每个CPU决定写入或读取内存/缓存的每个部分的时间是不确定的。

因此,对于CPU1:

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

让我们首先考虑编译器。编译器没有理由不重新排序或以其他方式更改这些功能。可能像:
temp_register = new Singleton(instanceCreated);
temp_register->setInstanceCreated();
instance = temp_register;

或许多其他可能性-如您所说的,只要观察到的单线程行为是一致的。该DOES包括“将ctor分解为两个函数,然后在下一个调用之前调用它的一部分,然后在随后的调用之前调用它的一部分”之类的事情。

现在,它可能不会分成2个调用,但是会内联ctor,特别是因为它是如此之小。然后,一旦内联,所有内容都可以重新排序,例如,好像ctor在2中被破坏了。

总的来说,我会说编译器不仅可能对事物进行重新排序,而且很有可能-也就是说,对于您拥有的代码,可能存在比该顺序“更好”的重新排序(一旦内联,并且可能是内联的)由C++代码给出。

但是,让我们将其放在一边,并尝试了解双重检查锁定的实际问题。
因此,我们假设编译器没有重新排序。那CPU呢?或更重要的是,CPU-复数。

第一个CPU“CPU1”需要遵循编译器给出的指令,特别是,它需要将已被告知要写入的内容写入内存:
  • instance
  • instanceCreated
  • Singleton的其他成员变量(即您的Singleton做某事,并且有一些状态,不是吗?)

  • 实际上,“其他成员变量”的内容非常重要。对您的单例重要-这才是真正的目的吗?对我们的讨论也很重要。因此,我们给它起一个名字:important_data。即instance->important_data。也许是instance->important_function(),它使用了important_data。等等。

    如前所述,我们假设编译器已经编写了代码,以便按照您期望的顺序来编写这些项目,即:
  • important_data-写在ctor内,从
    instance = new Singleton(instanceCreated);
  • instance-在new/ctor返回
  • 之后立即分配
  • instanceCreated-在setInstanceCreated()里面

  • 现在,CPU将这些写入移交给内存总线。知道内存总线做什么?它重新排序他们。 CPU和体系结构与编译器具有相同的约束-即确保该CPU始终看到所有内容-即单线程保持一致。因此,例如,如果instanceinstanceCreated在同一条缓存行上(实际上很有可能),则它们可能会写在一起,并且由于它们刚刚被读取,因此该缓存行是“热”的,所以也许它们被写入了首先在important_data之前,以便可以撤消该缓存行,以便为important_data所在的缓存行腾出空间。

    你看见了吗? instanceCreatedinstance刚刚在important_data之前提交到内存。请注意,CPU1不在乎,因为它生活在单线程世界中。

    因此,现在介绍CPU2:

    CPU2进来,看到instanceCreated == trueinstance != NULL并因此离开,并决定调用Singleton::Instance()-> important_function(),后者使用未初始化的important_data。崩溃砰砰声。

    顺便说一句,情况变得更糟。到目前为止,我们已经看到编译器可以重新排序,但是我们假装没有。让我们再往前走一步,假装CPU1没有对任何内存写入进行重新排序。现在可以了吗?

    不,当然不是。

    就像CPU1决定优化/重新排序其内存写入一样,CPU2可以重新排列其读取!

    CPU2进来看看
    if (!instanceCreated) ...
    

    因此需要读取instanceCreated。听说过“投机执行”吗? (顺便说一句,它是FPS游戏的好名字)。如果内存总线不忙于执行任何操作,则CPU2可能会预读一些其他值,“希望” instanceCreated为true。例如,它可以预读important_data。也许important_data(或分配器的未初始化内存,可能会变成important_data的分配器内存)已经在CPU2的高速缓存中。或者,也许(更有可能?)CPU2刚刚释放了该内存,并且分配器在其前4个字节中写入了NULL(分配器通常使用该内存作为其空闲列表),因此实际上,即将成为内存的important_data可能实际上仍然在CPU2的写队列中。在那种情况下,为什么CPU2甚至还没有完成写操作就烦恼重新读取该内存呢? (不会-它只会从其写入队列中获取值。)

    那有道理吗?如果不是,请想象instance(它是一个指针)的值为0x17e823d0。在成为(成为)单例汉之前,内存在做什么?该内存是否仍在CPU2的写入队列中?...

    或基本上,甚至不用考虑为什么会这样做,而是意识到CPU2可能会先读取important_data,然后再读取instanceCreated。因此,即使CPU1可能按顺序写了它们,但CPU2在important_data中看到“废话”,然后在true中看到了instanceCreated(谁知道instance中的内容!)。再说一次,CRASH BANG BOOM。或BOOM CRASH BANG,因为您现在已经意识到无法保证订单...

    关于c++ - 便携式线程安全的懒惰单例,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/1877745/

    10-10 08:46