Двойная проверка блокировки - Double-checked locking
В программная инженерия, двойная проверка блокировки (также известный как «оптимизация блокировки с двойной проверкой»[1]) это шаблон разработки программного обеспечения используется для уменьшения накладных расходов на приобретение замок проверяя критерий блокировки («подсказку блокировки») перед получением блокировки. Блокировка происходит только в том случае, если проверка критерия блокировки указывает, что блокировка требуется.
Шаблон, реализованный в некоторых сочетаниях языка и оборудования, может быть небезопасным. Иногда это можно считать антипаттерн.[2]
Обычно он используется для уменьшения накладных расходов на блокировку при реализации "ленивая инициализация "в многопоточной среде, особенно как часть Шаблон Singleton. Ленивая инициализация позволяет избежать инициализации значения до первого обращения к нему.
Использование в C ++ 11
Для шаблона singleton двойная проверка блокировки не требуется:
Если элемент управления входит в объявление одновременно во время инициализации переменной, параллельное выполнение должно ждать завершения инициализации.
— § 6.7 [stmt.dcl] p4
Синглтон& GetInstance() { статический Синглтон s; вернуть s;}
Если кто-то желает использовать идиому с двойной проверкой вместо тривиально работающего примера, приведенного выше (например, потому что Visual Studio до выпуска 2015 года не реализовывал язык стандарта C ++ 11 о параллельной инициализации, цитированный выше [3] ) необходимо использовать заборы захвата и снятия:[4]
#включают <atomic>#включают <mutex>класс Синглтон { общественный: Синглтон* GetInstance(); частный: Синглтон() = по умолчанию; статический стандартное::атомный<Синглтон*> s_instance; статический стандартное::мьютекс s_mutex;};Синглтон* Синглтон::GetInstance() { Синглтон* п = s_instance.грузить(стандартное::memory_order_acquire); если (п == nullptr) { // 1-я проверка стандартное::lock_guard<стандартное::мьютекс> замок(s_mutex); п = s_instance.грузить(стандартное::memory_order_relaxed); если (п == nullptr) { // 2-я (двойная) проверка п = новый Синглтон(); s_instance.магазин(п, стандартное::memory_order_release); } } вернуть п;}
Использование в Голанге
пакет основнойимпорт "синхронизировать"вар arrOnce синхронизировать.однаждывар обр []int// getArr получает arr, лениво инициализируя его при первом вызове. Дважды проверенный// блокировка реализована с помощью библиотечной функции sync.Once. Первый// горутина для победы в гонке вызов Do () инициализирует массив, а// другие будут блокироваться до завершения Do (). После запуска Do только// для получения массива потребуется одиночное атомарное сравнение.func getArr() []int { arrOnce.Делать(func() { обр = []int{0, 1, 2} }) вернуть обр}func основной() { // благодаря блокировке с двойной проверкой две горутины пытаются выполнить getArr () // не вызовет двойной инициализации идти getArr() идти getArr()}
Использование в Java
Рассмотрим, например, этот сегмент кода в Язык программирования Java как дано [2] (как и все остальные сегменты кода Java):
// Однопоточная версиякласс Фу { частный Помощник помощник; общественный Помощник getHelper() { если (помощник == значение NULL) { помощник = новый Помощник(); } вернуть помощник; } // другие функции и члены ...}
Проблема в том, что это не работает при использовании нескольких потоков. А замок должен быть получен в случае вызова двух потоков getHelper ()
одновременно. В противном случае либо они оба могут попытаться создать объект одновременно, либо один может получить ссылку на не полностью инициализированный объект.
Блокировка достигается дорогой синхронизацией, как показано в следующем примере.
// Правильная, но, возможно, дорогая многопоточная версиякласс Фу { частный Помощник помощник; общественный синхронизированный Помощник getHelper() { если (помощник == значение NULL) { помощник = новый Помощник(); } вернуть помощник; } // другие функции и члены ...}
Однако первый звонок в getHelper ()
создаст объект, и только несколько потоков, пытающихся получить к нему доступ в течение этого времени, должны быть синхронизированы; после этого все вызовы просто получают ссылку на переменную-член. поскольку синхронизация метода может в некоторых крайних случаях снизить производительность в 100 раз или больше,[5] накладные расходы на получение и снятие блокировки каждый раз, когда вызывается этот метод, кажутся ненужными: после завершения инициализации получение и снятие блокировок будет казаться ненужным. Многие программисты пытались оптимизировать эту ситуацию следующим образом:
- Убедитесь, что переменная инициализирована (без получения блокировки). Если он инициализирован, немедленно верните его.
- Получите замок.
- Дважды проверьте, была ли переменная уже инициализирована: если другой поток первым получил блокировку, возможно, он уже выполнил инициализацию. Если да, верните инициализированную переменную.
- В противном случае инициализируйте и верните переменную.
// Неработающая многопоточная версия// Идиома "Double-Checked Locking"класс Фу { частный Помощник помощник; общественный Помощник getHelper() { если (помощник == значение NULL) { синхронизированный (этот) { если (помощник == значение NULL) { помощник = новый Помощник(); } } } вернуть помощник; } // другие функции и члены ...}
Интуитивно этот алгоритм кажется эффективным решением проблемы. Однако у этого метода есть много тонких проблем, и его обычно следует избегать. Например, рассмотрим следующую последовательность событий:
- Нить А замечает, что значение не инициализировано, поэтому получает блокировку и начинает инициализировать значение.
- Из-за семантики некоторых языков программирования код, сгенерированный компилятором, может обновлять общую переменную, чтобы она указывала на частично построенный объект перед А завершил выполнение инициализации. Например, в Java, если вызов конструктора был встроен, общая переменная может быть немедленно обновлена после выделения хранилища, но до того, как встроенный конструктор инициализирует объект.[6]
- Нить B замечает, что общая переменная была инициализирована (или так кажется), и возвращает ее значение. Потому что нить B считает, что значение уже инициализировано, он не получает блокировку. Если B использует объект перед всей инициализацией, выполненной А видит B (либо потому что А не завершил инициализацию или потому что некоторые из инициализированных значений в объекте еще не проникли в память B использует (согласованность кеша )) программа скорее всего выйдет из строя.
Одна из опасностей использования блокировки с двойной проверкой J2SE 1.4 (и более ранние версии) заключается в том, что он часто оказывается работающим: нелегко отличить правильный реализация техники и тот, у которого есть тонкие проблемы. В зависимости от компилятор, чередование потоков планировщик и характер других одновременная системная активность, сбои в результате неправильной реализации блокировки с двойной проверкой могут возникать только периодически. Воспроизведение неудач может быть трудным.
По состоянию на J2SE 5.0, Эта проблема была исправлена. В летучий ключевое слово теперь гарантирует, что несколько потоков правильно обрабатывают экземпляр синглтона. Эта новая идиома описана в [3] и [4].
// Работает с семантикой получения / выпуска для volatile в Java 1.5 и новее// Не работает в семантике Java 1.4 и более ранних для volatileкласс Фу { частный летучий Помощник помощник; общественный Помощник getHelper() { Помощник localRef = помощник; если (localRef == значение NULL) { синхронизированный (этот) { localRef = помощник; если (localRef == значение NULL) { помощник = localRef = новый Помощник(); } } } вернуть localRef; } // другие функции и члены ...}
Обратите внимание на локальную переменную "localRef", что кажется ненужным. В результате в случаях, когда помощник уже инициализировано (т.е. большую часть времени), к полю volatile можно получить доступ только один раз (из-за "return localRef;" вместо того "вернуть помощника;"), что может улучшить общую производительность метода на 40 процентов.[7]
Java 9 представила VarHandle
класс, который позволяет использовать ослабленную атомику для доступа к полям, обеспечивая несколько более быстрое чтение на машинах со слабыми моделями памяти, за счет более сложной механики и потери последовательной согласованности (доступ к полям больше не участвует в порядке синхронизации, глобальном порядке доступ к изменчивым полям).[8]
// Работает с семантикой получения / выпуска для VarHandles, представленных в Java 9класс Фу { частный летучий Помощник помощник; общественный Помощник getHelper() { Помощник localRef = getHelperAcquire(); если (localRef == значение NULL) { синхронизированный (этот) { localRef = getHelperAcquire(); если (localRef == значение NULL) { localRef = новый Помощник(); setHelperRelease(localRef); } } } вернуть localRef; } частный статический окончательный VarHandle ПОМОЩЬ; частный Помощник getHelperAcquire() { вернуть (Помощник) ПОМОЩЬ.getAcquire(этот); } частный пустота setHelperRelease(Помощник ценность) { ПОМОЩЬ.setRelease(этот, ценность); } статический { пытаться { Метод: ручки.Искать искать = Метод: ручки.искать(); ПОМОЩЬ = искать.findVarHandle(Фу.класс, "помощник", Помощник.класс); } ловить (ReflectiveOperationException е) { бросить новый ExceptionInInitializerError(е); } } // другие функции и члены ...}
Если вспомогательный объект является статическим (по одному на загрузчик классов), альтернативой является Идиома держателя инициализации по требованию[9] (См. Листинг 16.6.[10] из ранее процитированного текста.)
// Правильная отложенная инициализация в Javaкласс Фу { частный статический класс HelperHolder { общественный статический окончательный Помощник помощник = новый Помощник(); } общественный статический Помощник getHelper() { вернуть HelperHolder.помощник; }}
Это основано на том факте, что вложенные классы не загружаются, пока на них нет ссылки.
Семантика окончательный поле в Java 5 можно использовать для безопасной публикации вспомогательного объекта без использования летучий:[11]
общественный класс FinalWrapper<Т> { общественный окончательный Т ценность; общественный FinalWrapper(Т ценность) { этот.ценность = ценность; }}общественный класс Фу { частный FinalWrapper<Помощник> helperWrapper; общественный Помощник getHelper() { FinalWrapper<Помощник> tempWrapper = helperWrapper; если (tempWrapper == значение NULL) { синхронизированный (этот) { если (helperWrapper == значение NULL) { helperWrapper = новый FinalWrapper<Помощник>(новый Помощник()); } tempWrapper = helperWrapper; } } вернуть tempWrapper.ценность; }}
Локальная переменная tempWrapper требуется для корректности: просто используя helperWrapper как для нулевых проверок, так и для оператора return может произойти сбой из-за переупорядочения чтения, разрешенного в модели памяти Java.[12] Производительность этой реализации не обязательно лучше, чем у летучий реализация.
Использование в C #
Блокировка с двойной проверкой может быть эффективно реализована в .NET. Распространенным шаблоном использования является добавление блокировки с двойной проверкой к реализациям Singleton:
общественный класс MySingleton{ частный статический объект _myLock = новый объект(); частный статический MySingleton _mySingleton = значение NULL; частный MySingleton() { } общественный статический MySingleton GetInstance() { если (_mySingleton == значение NULL) // Первая проверка { замок (_myLock) { если (_mySingleton == значение NULL) // Вторая (двойная) проверка { _mySingleton = новый MySingleton(); } } } вернуть mySingleton; }}
В этом примере «подсказка блокировки» - это объект mySingleton, который больше не является нулевым, когда он полностью построен и готов к использованию.
В .NET Framework 4.0 Ленивый
был представлен класс, который по умолчанию использует блокировку с двойной проверкой (режим ExecutionAndPublication) для хранения либо исключения, которое было сгенерировано во время построения, либо результата функции, которая была передана в Ленивый
:[13]
общественный класс MySingleton{ частный статический только чтение Ленивый<MySingleton> _mySingleton = новый Ленивый<MySingleton>(() => новый MySingleton()); частный MySingleton() { } общественный статический MySingleton Пример => _mySingleton.Ценность;}
Смотрите также
- В Тест и тест-установка идиома для блокирующего механизма низкого уровня.
- Идиома держателя инициализации по требованию для поточно-ориентированной замены в Java.
использованная литература
- ^ Schmidt, D et al. Шаблонно-ориентированная архитектура программного обеспечения Том 2, 2000, стр. 353-363
- ^ а б Дэвид Бэкон и др. Декларация «Двойная проверка блокировки нарушена».
- ^ «Поддержка функций C ++ 11-14-17 (современный C ++)».
- ^ Двойная проверка блокировки исправлена в C ++ 11
- ^ Бём, Ханс-Дж (июнь 2005 г.). «Потоки не могут быть реализованы как библиотека» (PDF). Уведомления ACM SIGPLAN. 40 (6): 261–268. Дои:10.1145/1064978.1065042.
- ^ Хаггар, Питер (1 мая 2002 г.). «Двойная проверка блокировки и шаблон Singleton». IBM.
- ^ Джошуа Блох "Эффективная Java, третье издание", стр. 372
- ^ «Глава 17. Потоки и замки». docs.oracle.com. Получено 2018-07-28.
- ^ Брайан Гетц и др. Параллелизм Java на практике, 2006, стр. 348
- ^ Гетц, Брайан; и другие. «Java Concurrency на практике - списки на сайте». Получено 21 октября 2014.
- ^ [1] Список рассылки Javamemorymodel-обсуждения
- ^ [2] Мэнсон, Джереми (2008-12-14). "Отложенная инициализация Date-Race-Ful для повышения производительности - параллелизм Java (и c)". Получено 3 декабря 2016.
- ^ Альбахари, Джозеф (2010). «Потоки в C #: Использование потоков». C # 4.0 в двух словах. O'Reilly Media. ISBN 978-0-596-80095-6.
Ленивый
фактически реализует […] блокировку с двойной проверкой. Блокировка с двойной проверкой выполняет дополнительное энергозависимое чтение, чтобы избежать затрат на получение блокировки, если объект уже инициализирован.
внешние ссылки
- Проблемы с механизмом блокировки с двойной проверкой зафиксированы в Блоги Jeu George
- Описание "Double Checked Locking" из Портлендского репозитория шаблонов
- "Двойная проверка блокировки нарушена" Описание из Портлендского репозитория шаблонов
- Бумага "C ++ и опасности двойной проверки блокировки "(475 КБ) Скотт Мейерс и Андрей Александреску
- Статья "Двойная проверка блокировки: умно, но сломано " от Брайан Гетц
- Статья "Предупреждение! Многопоточность в многопроцессорном мире " от Аллен Голуб
- Двойная проверка блокировки и шаблон Singleton
- Шаблон Singleton и безопасность потоков
- ключевое слово volatile в VC ++ 2005
- Примеры Java и сроки решений двойной проверки блокировки
- «Более эффективная Java с Джошуа Блохом из Google».