Что не так с этим исправлением блокировки с двойной проверкой?

StackOverflow https://stackoverflow.com/questions/945232

Вопрос

Итак, сейчас я видел много статей, в которых утверждается, что на C ++ нарушена блокировка с двойной проверкой, обычно используемая для предотвращения попыток нескольких потоков инициализировать лениво созданный синглтон.Обычный дважды проверенный код блокировки читается следующим образом:

class singleton {
private:
    singleton(); // private constructor so users must call instance()
    static boost::mutex _init_mutex;

public:
    static singleton & instance()
    {
        static singleton* instance;

        if(!instance)
        {
            boost::mutex::scoped_lock lock(_init_mutex);

            if(!instance)           
                instance = new singleton;
        }

        return *instance;
    }
};

Проблема, по-видимому, заключается в экземпляре назначения строки - компилятор волен выделить объект, а затем назначить на него указатель, ИЛИ установить указатель на то место, где он будет выделен, а затем выделить его.Последний случай нарушает идиому - один поток может выделить память и назначить указатель, но не запускать конструктор singleton до того, как он будет переведен в спящий режим - тогда второй поток увидит, что экземпляр не равен null, и попытается вернуть его, даже если он еще не был создан.

Я увидел предложение использовать локальное логическое значение потока и проверить, что вместо instance.Что - то вроде этого:

class singleton {
private:
    singleton(); // private constructor so users must call instance()
    static boost::mutex _init_mutex;
    static boost::thread_specific_ptr<int> _sync_check;

public:
    static singleton & instance()
    {
        static singleton* instance;

        if(!_sync_check.get())
        {
            boost::mutex::scoped_lock lock(_init_mutex);

            if(!instance)           
                instance = new singleton;

            // Any non-null value would work, we're really just using it as a
            // thread specific bool.
            _sync_check = reinterpret_cast<int*>(1);
        }

        return *instance;
    }
};

Таким образом, каждый поток в конечном итоге проверяет, был ли экземпляр создан один раз, но после этого останавливается, что влечет за собой некоторое снижение производительности, но все же не так сильно, как блокировка каждого вызова.Но что, если бы мы просто использовали локальный статический bool?:

class singleton {
private:
    singleton(); // private constructor so users must call instance()
    static boost::mutex _init_mutex;

public:
    static singleton & instance()
    {
        static bool sync_check = false;
        static singleton* instance;

        if(!sync_check)
        {
            boost::mutex::scoped_lock lock(_init_mutex);

            if(!instance)           
                instance = new singleton;

            sync_check = true;
        }

        return *instance;
    }
};

Почему бы этому не сработать?Даже если sync_check должен был быть прочитан одним потоком, когда он был назначен в другом, мусорное значение все равно будет отличным от нуля и, следовательно, истинным. Этот докторСтатья Добба утверждает, что вам нужно заблокировать, потому что вы никогда не выиграете битву с компилятором за переупорядочение инструкций.Это заставляет меня думать, что по какой-то причине это не должно работать, но я не могу понять почему.Если требования к точкам последовательности столь же низки, как и требования к Dr.Статья Добба заставляет меня поверить, я не понимаю почему Любой код после блокировки не может быть переупорядочен так, чтобы он был перед блокировкой.Что привело бы к прерыванию периода многопоточности C ++.

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

Так сработает это или нет?Почему?

Это было полезно?

Решение

Ваше исправление ничего не исправляет, поскольку запись в sync_check и экземпляр может выполняться не по порядку на ЦП.В качестве примера представьте, что первые два вызова экземпляра происходят примерно в одно и то же время на двух разных процессорах.Первый поток получит блокировку, инициализирует указатель и установит для sync_check значение true именно в этом порядке, но процессор может изменить порядок записи в память.На другом ЦП второй поток может проверить sync_check и убедиться, что это правда, но экземпляр еще не может быть записан в память.Видеть Рекомендации по программированию без блокировки для Xbox 360 и Microsoft Windows для получения подробной информации.

Тогда упомянутое вами решение sync_check для конкретного потока должно работать (при условии, что вы инициализируете указатель равным 0).

Другие советы

Здесь есть отличное чтение об этом (хотя оно ориентировано на .net / c #): http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

Это сводится к тому, что вам нужно иметь возможность сообщить процессору, что он не может изменить порядок ваших операций чтения / записи для доступа к этой переменной (начиная с оригинального Pentium, процессор может изменить порядок определенных инструкций, если он считает, что логика не будет затронута), и что ему необходимо обеспечить согласованность кэша (не забывайте об этом - мы, разработчики, можем притворяться, что вся память - это всего лишь один плоский ресурс, но на самом деле, у каждого ядра процессора есть кэш, некоторые неразделенные (L1), некоторые могут иногда использоваться совместно (L2)) - ваша инициализация может выполняться запись в основную оперативную память, но другое ядро может иметь неинициализированное значение в кэше.Если у вас нет никакой семантики параллелизма, процессор может не знать, что его кэш загрязнен.

Я не знаю сторону C ++, но в .net вы бы обозначили переменную как volatile, чтобы защитить доступ к ней (или вы бы использовали методы барьера чтения / записи в памяти в System.Многопоточность).

Кроме того, я читал, что в .net 2.0 двойная проверка блокировки гарантированно работает без "изменчивых" переменных (для любых читателей .net) - это не поможет вам с вашим кодом на c ++.

Если вы хотите быть в безопасности, вам нужно будет сделать эквивалент c ++ для обозначения переменной как volatile в c #.

"Последний случай нарушает идиому - два потока могут в конечном итоге создать синглтон".

Но если я правильно понимаю код, в первом примере вы проверяете, существует ли уже экземпляр (может выполняться несколькими потоками одновременно), если этого не происходит, один поток блокирует его и создает экземпляр - только один поток может выполнить создание в это время.Все остальные потоки блокируются и будут ждать.

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

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

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

Я что-то здесь упускаю?

Хорошо, не уверен насчет переупорядочивания операций, но в данном случае это изменило бы логику, поэтому я бы не ожидал, что это произойдет - но я не эксперт в этой теме.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top