2

我首先要说我已经知道在 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 ,但似乎没有人有任何解决方案。任何人提到的唯一解决方案是使用互斥锁来防止来自多个线程的初始化。但这似乎是先有鸡还是先有蛋的问题。我知道的每种类型的互斥体都需要进行某种初始化才能使用。在我第一次使用它之前,我如何确保它被初始化。我想我必须将其设为静态本地。但是如何确保它从一个线程初始化?

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

4

1 回答 1

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对象时,这可能是一个问题(例如,因为它由一些基本的单例资源支持,也许是一些硬件资源的句柄)。如果创建多个,也会有一些浪费的工作Instance,如果创建过程很昂贵,这可能很重要。

这种模式有时被称为racy single-check

使用延迟初始化的互斥体

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

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

您可以GetInstance使用双重检查锁定以牺牲一些复杂性为代价来加快速度:与其CRITICAL_SECTION无条件地获取,不如先检查是否Instance已设置(如第一个示例),然后如果是则直接返回它。

初始化一次执行一次

最后,如果您的目标是 Windows Vista 或更高版本,Microsoft 添加了一个直接处理此问题的现成工具:InitOnceExecuteOnce。您可以在此处找到一个工作示例。这与 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 写了一篇关于此功能的博客文章,这也有助于阅读。

于 2018-05-09T22:01:21.413 回答