VC ++ 6 потокобезопасная статическая инициализация - PullRequest
0 голосов
/ 10 мая 2018

Начну с того, что уже знаю, что в стандарте 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;
    }
};

Я думал, что это решило проблему, но теперь я обнаружил, что вещи не всегда инициализируются должным образом, потому что я создаю другие потоки во время запуска.

Рэймонд Чен описал проблему в 2004 году: https://blogs.msdn.microsoft.com/oldnewthing/20040308-00/?p=40363, но ни у кого, казалось, не было никаких решений. Единственные решения, которые кто-либо упоминает, - это использование мьютексов для предотвращения инициализации из нескольких потоков. Но это похоже на проблему курицы и яйца. Каждый тип мьютекса, который мне известен, требует некоторой инициализации, прежде чем его можно будет использовать. Как я могу убедиться, что он инициализируется, прежде чем я использую его в первый раз. Я думаю, мне нужно сделать его статическим локальным. Но как мне обеспечить его инициализацию из одного потока?

Если бы я мог убедиться, что у меня есть одна ячейка памяти, инициализированная с известным значением, прежде чем что-либо еще будет инициализировано, я мог бы использовать взаимосвязанные операции над ней, чтобы вращать ожидание, чтобы загрузить всю мою инициализацию. Есть ли способ убедиться, что одна ячейка памяти находится в известном состоянии из нескольких потоков, прежде чем произойдет какая-либо другая инициализация? Или какая-нибудь синхронизация, которая может быть выполнена без проблем с курицей и яйцом?

1 Ответ

0 голосов
/ 10 мая 2018

Обычное решение этой проблемы - использовать статический объект, который можно инициализировать нулем или инициализировать константой в сочетании с атомарными операциями, чтобы «загрузить» себя в положение, в котором вы можете безопасно вызывать более сложную инициализацию.

Нулевая и постоянная инициализация гарантированно произойдут до непостоянной инициализации, и, поскольку она фактически происходит одновременно, она не зависит от порядка инициализации.

Использовать Ленивый Инициализированный Объект

В очень простом примере будет использоваться инициализированный нулем указатель на глобальный статический экземпляр, который указывает, была ли статическая инициализирована, например:

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 - это многоразовая оболочка для объекта Windows CRITICAL_SECTION, которая выполняет отложенную и поточно-ориентированную инициализацию внутри метода get() и может быть инициализирована нулями. Этот MutexHolder затем используется как классический мьютекс для защиты создания статического A объекта внутри A::GetInstance.

Вы можете сделать GetInstance быстрее за счет некоторой сложности с помощью двойной проверки блокировки : вместо того, чтобы безоговорочно получить CRITICAL_SECTION, сначала проверьте, установлен ли Instance (например, первый пример), а затем вернуть его напрямую, если он есть.

InitOnceExecuteOnce

Наконец, если вы ориентируетесь на 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;
    }
};        

Раймонд Чен написал в блоге запись об этой функции , которая также способствует хорошему чтению.

...