VC ++ 6线程安全静态初始化

时间:2018-05-09 21:39:56

标签: c++ multithreading 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()将返回一个未初始化的实例。所以我遵循了这个建议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,但似乎没有人有任何解决方案。任何人提到的唯一解决方案是使用互斥锁来防止多线程初始化。但这似乎是鸡和蛋的问题。我所知道的每种类型的互斥锁都需要进行某种初始化才能使用它。在第一次使用它之前,我怎样才能确保它被初始化。我想我必须让它静态本地化。但是如何确保从一个线程初始化它?

如果我可以确保在初始化任何其他内容之前将一个内存位置初始化为已知值,我可以使用互锁操作来旋转等待引导我的整个初始化。在任何其他初始化发生之前,有没有办法确保一个内存位置在多个线程中处于已知状态?或者可以在没有鸡和蛋问题的情况下进行任何同步?

1 个答案:

答案 0 :(得分:1)

这个问题的通常解决方案是使用一个静态对象,该对象可以通过零初始化或常量初始化与原子操作相结合,将自己“引导”到一个可以安全调用更复杂初始化的位置。

保证零和常量初始化在非常量初始化之前发生,因为它实际上同时发生,它不依赖于初始化顺序。

使用延迟初始化对象

一个非常简单的示例将使用指向全局静态实例的零初始化指针,指示静态是否已初始化,如下所示:

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视为空,则可能会创建两个(或更多)一个A::Instance对象。代码只能正确选择一个A对象作为返回给所有调用者的真正静态全局,而其他对象只是默默地删除,但这可能是一个问题,甚至不可能创建多个{{进程中的对象(例如,因为它由一些基本的单例资源支持,可能是某些硬件资源的句柄)。如果创建了多个Instance,也会有一些浪费的工作,如果创建过程很昂贵,这可能很重要。

此模式有时称为 racy single-check

使用延迟初始化的互斥锁

避免上述陷阱的更好解决方案是使用互斥锁来保护单例的创建。当然,现在互斥初始化有相同的排序问题,但是我们可以使用上面的技巧来修复它(我们知道可以创建多个互斥对象)。

Instance

这里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; } }; 是围绕Windows MutexHolder对象的可重用包装器,它在CRITICAL_SECTION方法内执行惰性和线程安全初始化,并且可以进行零初始化。然后将此get()用作经典互斥锁,以保护MutexHolder内的静态A对象的创建。

使用double-checked locking,您可以更快地提高A::GetInstance代价:而不是无条件地获取GetInstance,首先检查是否设置了CRITICAL_SECTION(比如第一个例子)然后直接返回它。

InitOnceExecuteOnce

最后,如果您的目标是Windows Vista或更高版本,Microsoft已添加了一个直接处理此问题的现成工具:InitOnceExecuteOnce。你可以找到一个worked example here。这与Instance类似的POSIX是有效的,因为初始化是使用常量pthead_once执行的。

在你的情况下,它看起来像:

INIT_ONCE_STATIC_INIT

Raymond Chen写了一篇blog entry about this function,这也是一本很好的阅读材料。