VC++6线程安全静态初始化
VC++6 thread safe static initialization
我首先要说的是,我已经知道在 C++11 标准中,静态局部初始化现在是线程安全的。但是,我仍然需要保持与 Microsoft Visual C++ 6 的兼容性,因此 C++11 行为不适用。
我有一个使用少量静态变量的静态库。我 运行 遇到静态变量在初始化之前被使用的问题(单线程):
class A
{
private:
static A Instance;
public:
static A& GetInstance() { return Instance; }
};
// And then from a different file:
A.GetInstance();
A.GetInstance() 会 return 一个未初始化的实例。所以我听从了这个建议 http://www.cs.technion.ac.il/users/yechiel/c++-faq/static-init-order-on-first-use-members.html 并将我所有的静态变量移动到本地方法中。
class A
{
public:
static A& GetInstance()
{
static A Instance;
return Instance;
}
};
我以为这解决了问题,但现在我发现有些东西并不总是正确初始化,因为我在启动期间创建了其他线程。
Raymond Chen 在 2004 年描述了这个问题:https://blogs.msdn.microsoft.com/oldnewthing/20040308-00/?p=40363,但似乎没有人有任何解决方案。任何人提到的唯一解决方案是使用互斥锁来防止从多个线程进行初始化。但这似乎是先有鸡还是先有蛋的问题。我所知道的每种类型的互斥锁都需要某种初始化才能使用。我怎样才能确保它在我第一次使用它之前被初始化。我想我必须将其设为本地静态。但是如何确保它从一个线程初始化?
如果我可以确保在初始化任何其他内容之前将一个内存位置初始化为已知值,我可以对其使用互锁操作来旋转等待 bootstrap 我的整个初始化。有没有办法确保一个内存位置在任何其他初始化发生之前处于多个线程的已知状态?或者任何一种可以在没有先有鸡还是先有蛋的问题的情况下完成的同步?
这个问题的通常解决方案是使用一个可以零初始化或常量初始化的静态对象,并结合原子操作"bootstrap"让自己进入一个可以安全地调用更复杂的初始化的位置。
零和常量初始化保证在非常量初始化之前发生,因为它实际上同时发生,所以不依赖于初始化顺序。
使用延迟初始化的对象
一个非常简单的示例将使用指向全局静态实例的零初始化指针,指示静态是否已初始化,如下所示:
class A
{
private:
volatile static A* Instance; // zero-initialized to NULL
public:
static A& GetInstance() {
A* inst = Instance;
if (!inst) {
A* inst = new Instance(...);
A* cur = InterlockedCompareExchange(&Instance, newInst, 0);
if (cur) {
delete inst;
return *cur;
}
}
return *inst;
}
};
上述方法的缺点是,如果两个(或更多)线程最初都将 A::Instance
视为 null,则可能会创建两个(或更多)一个 A
对象。该代码仅正确选择了一个 A
对象作为真正的静态全局对象 returned 给所有调用者,而其他对象则被简单地静默删除,但这可能是一个问题,因为它甚至无法创建进程中有多个 Instance
对象(例如,因为它由一些基本的单例资源支持,可能是某些硬件资源的句柄)。如果创建多个 Instance
,也会有一些浪费的工作,如果创建过程很昂贵,这可能很重要。
这种模式有时被称为 racy single-check。
使用延迟初始化的互斥锁
避免上述陷阱的更好解决方案是使用互斥锁来保护单例的创建。当然,现在 mutex 初始化有同样的顺序问题,但我们可以使用上面的技巧来解决这个问题(我们知道创建多个 mutex 对象是可以的)。
class MutexHolder
{
private:
volatile static CRITICAL_SECTION* cs; // zero-initialized to NULL
public:
static CRITICAL_SECTION* get() {
A* inst = cs;
if (!inst) {
CRITICAL_SECTION* inst = new CRITICAL_SECTION();
InitializeCriticalSection(inst);
CRITICAL_SECTION* cur = InterlockedCompareExchange(&cs, newInst, 0);
if (cur) {
DeleteCriticalSection(inst);
delete inst;
return *cur;
}
}
return *inst;
}
};
class A
{
private:
static MutexHolder mutex;
static A* Instance; // zero-initialized to NULL
public:
static A& GetInstance() {
A* inst;
CRITICAL_SECTION *cs = mutex.get();
EnterCriticalSection(cs);
if (!(inst = Instance)) {
inst = Instance = new A(...);
}
EnterCriticalSection(cs);
return inst;
}
};
这里的 MutexHolder
是围绕 Windows CRITICAL_SECTION
对象的可重用包装器,它在 get()
方法中执行延迟和线程安全的初始化,并且可以被零初始化。然后,此 MutexHolder
用作经典互斥锁,以保护 A::GetInstance
.
中静态 A
对象的创建
您可以通过使用 double-checked locking 以一些复杂性为代价使 GetInstance
更快:而不是无条件地获取 CRITICAL_SECTION
,首先检查 Instance
是否是设置(如第一个示例)然后 return 如果是,则直接设置它。
InitOnceExecuteOnce
最后,如果您的目标是 Windows Vista 或更高版本,Microsoft 添加了一个现成的工具来直接处理此问题:InitOnceExecuteOnce. You can find an worked example here。这与 POSIX 与 pthead_once
大致类似并且有效,因为初始化是使用常量 INIT_ONCE_STATIC_INIT
.
执行的
在你的情况下它看起来像:
INIT_ONCE g_InitOnce = INIT_ONCE_STATIC_INIT;
A* g_AInstance = 0;
BOOL CALLBACK MakeA(
PINIT_ONCE InitOnce,
PVOID Parameter,
PVOID *lpContext)
{
g_AInstance = new A(...);
return TRUE;
}
class A
{
private:
public:
static A& GetInstance() {
// Execute the initialization callback function
bStatus = InitOnceExecuteOnce(&g_InitOnce,
MakeA,
NULL,
NULL);
assert(bStatus);
return *g_AInstance;
}
};
Raymond Chen 写了一个 blog entry about this function 也很适合阅读。
我首先要说的是,我已经知道在 C++11 标准中,静态局部初始化现在是线程安全的。但是,我仍然需要保持与 Microsoft Visual C++ 6 的兼容性,因此 C++11 行为不适用。
我有一个使用少量静态变量的静态库。我 运行 遇到静态变量在初始化之前被使用的问题(单线程):
class A
{
private:
static A Instance;
public:
static A& GetInstance() { return Instance; }
};
// And then from a different file:
A.GetInstance();
A.GetInstance() 会 return 一个未初始化的实例。所以我听从了这个建议 http://www.cs.technion.ac.il/users/yechiel/c++-faq/static-init-order-on-first-use-members.html 并将我所有的静态变量移动到本地方法中。
class A
{
public:
static A& GetInstance()
{
static A Instance;
return Instance;
}
};
我以为这解决了问题,但现在我发现有些东西并不总是正确初始化,因为我在启动期间创建了其他线程。
Raymond Chen 在 2004 年描述了这个问题:https://blogs.msdn.microsoft.com/oldnewthing/20040308-00/?p=40363,但似乎没有人有任何解决方案。任何人提到的唯一解决方案是使用互斥锁来防止从多个线程进行初始化。但这似乎是先有鸡还是先有蛋的问题。我所知道的每种类型的互斥锁都需要某种初始化才能使用。我怎样才能确保它在我第一次使用它之前被初始化。我想我必须将其设为本地静态。但是如何确保它从一个线程初始化?
如果我可以确保在初始化任何其他内容之前将一个内存位置初始化为已知值,我可以对其使用互锁操作来旋转等待 bootstrap 我的整个初始化。有没有办法确保一个内存位置在任何其他初始化发生之前处于多个线程的已知状态?或者任何一种可以在没有先有鸡还是先有蛋的问题的情况下完成的同步?
这个问题的通常解决方案是使用一个可以零初始化或常量初始化的静态对象,并结合原子操作"bootstrap"让自己进入一个可以安全地调用更复杂的初始化的位置。
零和常量初始化保证在非常量初始化之前发生,因为它实际上同时发生,所以不依赖于初始化顺序。
使用延迟初始化的对象
一个非常简单的示例将使用指向全局静态实例的零初始化指针,指示静态是否已初始化,如下所示:
class A
{
private:
volatile static A* Instance; // zero-initialized to NULL
public:
static A& GetInstance() {
A* inst = Instance;
if (!inst) {
A* inst = new Instance(...);
A* cur = InterlockedCompareExchange(&Instance, newInst, 0);
if (cur) {
delete inst;
return *cur;
}
}
return *inst;
}
};
上述方法的缺点是,如果两个(或更多)线程最初都将 A::Instance
视为 null,则可能会创建两个(或更多)一个 A
对象。该代码仅正确选择了一个 A
对象作为真正的静态全局对象 returned 给所有调用者,而其他对象则被简单地静默删除,但这可能是一个问题,因为它甚至无法创建进程中有多个 Instance
对象(例如,因为它由一些基本的单例资源支持,可能是某些硬件资源的句柄)。如果创建多个 Instance
,也会有一些浪费的工作,如果创建过程很昂贵,这可能很重要。
这种模式有时被称为 racy single-check。
使用延迟初始化的互斥锁
避免上述陷阱的更好解决方案是使用互斥锁来保护单例的创建。当然,现在 mutex 初始化有同样的顺序问题,但我们可以使用上面的技巧来解决这个问题(我们知道创建多个 mutex 对象是可以的)。
class MutexHolder
{
private:
volatile static CRITICAL_SECTION* cs; // zero-initialized to NULL
public:
static CRITICAL_SECTION* get() {
A* inst = cs;
if (!inst) {
CRITICAL_SECTION* inst = new CRITICAL_SECTION();
InitializeCriticalSection(inst);
CRITICAL_SECTION* cur = InterlockedCompareExchange(&cs, newInst, 0);
if (cur) {
DeleteCriticalSection(inst);
delete inst;
return *cur;
}
}
return *inst;
}
};
class A
{
private:
static MutexHolder mutex;
static A* Instance; // zero-initialized to NULL
public:
static A& GetInstance() {
A* inst;
CRITICAL_SECTION *cs = mutex.get();
EnterCriticalSection(cs);
if (!(inst = Instance)) {
inst = Instance = new A(...);
}
EnterCriticalSection(cs);
return inst;
}
};
这里的 MutexHolder
是围绕 Windows CRITICAL_SECTION
对象的可重用包装器,它在 get()
方法中执行延迟和线程安全的初始化,并且可以被零初始化。然后,此 MutexHolder
用作经典互斥锁,以保护 A::GetInstance
.
A
对象的创建
您可以通过使用 double-checked locking 以一些复杂性为代价使 GetInstance
更快:而不是无条件地获取 CRITICAL_SECTION
,首先检查 Instance
是否是设置(如第一个示例)然后 return 如果是,则直接设置它。
InitOnceExecuteOnce
最后,如果您的目标是 Windows Vista 或更高版本,Microsoft 添加了一个现成的工具来直接处理此问题:InitOnceExecuteOnce. You can find an worked example here。这与 POSIX 与 pthead_once
大致类似并且有效,因为初始化是使用常量 INIT_ONCE_STATIC_INIT
.
在你的情况下它看起来像:
INIT_ONCE g_InitOnce = INIT_ONCE_STATIC_INIT;
A* g_AInstance = 0;
BOOL CALLBACK MakeA(
PINIT_ONCE InitOnce,
PVOID Parameter,
PVOID *lpContext)
{
g_AInstance = new A(...);
return TRUE;
}
class A
{
private:
public:
static A& GetInstance() {
// Execute the initialization callback function
bStatus = InitOnceExecuteOnce(&g_InitOnce,
MakeA,
NULL,
NULL);
assert(bStatus);
return *g_AInstance;
}
};
Raymond Chen 写了一个 blog entry about this function 也很适合阅读。