祝福大家。
我正在尝试编写线程安全的懒惰单例以供将来使用。这是我能想到的最好的方法。任何人都可以发现它的任何问题吗?关键假设是静态初始化在动态初始化之前在单个线程中发生。 (这将用于商业项目,而公司不使用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
实际上,“其他成员变量”的内容非常重要。对您的单例重要-这才是真正的目的吗?对我们的讨论也很重要。因此,我们给它起一个名字:
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始终看到所有内容-即单线程保持一致。因此,例如,如果
instance
和instanceCreated
在同一条缓存行上(实际上很有可能),则它们可能会写在一起,并且由于它们刚刚被读取,因此该缓存行是“热”的,所以也许它们被写入了首先在important_data
之前,以便可以撤消该缓存行,以便为important_data
所在的缓存行腾出空间。你看见了吗?
instanceCreated
和instance
刚刚在important_data
之前提交到内存。请注意,CPU1不在乎,因为它生活在单线程世界中。因此,现在介绍CPU2:
CPU2进来,看到
instanceCreated == true
和instance != 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/