В программной инженерии , перепроверил замок (также известный как «двойной проверку блокировки оптимизации» [1] ) является шаблон проектирования программного обеспечения используется для уменьшения накладных расходов на приобретение блокировки путем проверки критерия запирающего ( «замочная скважина намек») перед приобретение замка. Блокировка происходит только в том случае, если проверка критерия блокировки указывает, что блокировка требуется.
Шаблон, реализованный в некоторых сочетаниях языка и оборудования, может быть небезопасным. Иногда это можно рассматривать как антипаттерн . [2]
Обычно он используется для уменьшения накладных расходов на блокировку при реализации « ленивой инициализации » в многопоточной среде, особенно как часть шаблона Singleton . Ленивая инициализация позволяет избежать инициализации значения до первого обращения к нему.
Использование в C ++ 11 [ править ]
Для одноэлементного шаблона блокировка с двойной проверкой не требуется:
Если элемент управления входит в объявление одновременно во время инициализации переменной, параллельное выполнение должно дождаться завершения инициализации.
- § 6.7 [stmt.dcl] p4
Синглтон и GetInstance () { static Singleton s ; return s ; }
C ++ 11 и более поздние версии также предоставляют встроенный шаблон блокировки с двойной проверкой в виде std::once_flag
и std::call_once
:
#include <мьютекс>#include <optional> // Начиная с C ++ 17// Singleton.h class Singleton { public : static Singleton * GetInstance (); частный : Singleton () = по умолчанию ; статический std :: optional < Синглтон > s_instance ; статический std :: once_flag s_flag ; };// Синглтон.cpp std :: optional < Синглтон > Синглтон :: s_instance ; std :: once_flag Синглтон :: s_flag {};Синглтон * Синглтон :: GetInstance () { std :: call_once ( Синглтон :: s_flag , [] () { s_instance . Emplace ( Синглтон {}); }); return & * s_instance ; }
Если кто-то действительно желает использовать идиому с двойной проверкой вместо тривиально работающего примера, приведенного выше (например, потому что Visual Studio до выпуска 2015 года не реализовывала язык стандарта C ++ 11 о параллельной инициализации, цитируемый выше [3] ), вам потребуется использовать ограждения захвата и освобождения: [4]
#include <атомарный>#include <мьютекс>класс Синглтон { общедоступный : статический Синглтон * GetInstance (); частный : Singleton () = по умолчанию ; статический std :: atomic < Синглтон *> s_instance ; статический std :: mutex s_mutex ; };Синглтон * Синглтон :: GetInstance () { Синглтон * p = s_instance . загрузка ( std :: memory_order_acquire ); if ( p == nullptr ) { // 1-я проверка std :: lock_guard < std :: mutex > lock ( s_mutex ); p = s_instance . загрузка ( std :: memory_order_relaxed ); если ( p == nullptr ) { // 2-я (двойная) проверка p = new Singleton (); s_instance . store ( p , std :: memory_order_release ); } } return p ; }
Использование в Go [ править ]
основной пакетимпортировать "синхронизировать"var arrOnce sync . Один раз var arr [] int// getArr извлекает arr, лениво инициализируя его при первом вызове. // Блокировка с двойной проверкой реализована с помощью библиотечной функции sync.Once. Первая // горутина, выигравшая гонку за вызовом Do (), инициализирует массив, // а остальные будут блокироваться, пока Do () не завершится. После запуска Do потребуется // только одно атомарное сравнение, чтобы получить массив. func getArr () [] int { arrOnce . Do ( func () { arr = [] int { 0 , 1 , 2 } }) return arr }func main () { // благодаря блокировке с двойной проверкой две горутины, пытающиеся выполнить getArr () // не вызовут двойной инициализации go getArr () go getArr () }
Использование в Java [ править ]
Рассмотрим, например, этот сегмент кода на языке программирования Java, указанный в [2] (а также все другие сегменты кода Java):
// Однопоточная версия class Foo { private Helper helper ; общедоступный помощник getHelper () { если ( помощник == нуль ) { помощник = новый помощник (); } return helper ; } // другие функции и члены ... }
Проблема в том, что это не работает при использовании нескольких потоков. Замок должен быть получен в случае , если два потока вызывает getHelper()
одновременно. В противном случае либо они оба могут попытаться создать объект одновременно, либо один может получить ссылку на не полностью инициализированный объект.
Блокировка достигается за счет дорогостоящей синхронизации, как показано в следующем примере.
// Правильная, но, возможно, дорогая многопоточная версия class Foo { private Helper helper ; общедоступный синхронизированный помощник getHelper () { если ( помощник == null ) { помощник = новый помощник (); } return helper ; } // другие функции и члены ... }
Однако первый вызов getHelper()
создаст объект, и только несколько потоков, пытающихся получить к нему доступ в течение этого времени, должны быть синхронизированы; после этого все вызовы просто получают ссылку на переменную-член. Поскольку синхронизация метода может в некоторых крайних случаях снизить производительность в 100 или более раз, [5] накладные расходы на получение и снятие блокировки каждый раз, когда вызывается этот метод, кажутся ненужными: после завершения инициализации получение и освобождение замки казались бы ненужными. Многие программисты пытались оптимизировать эту ситуацию следующим образом:
- Убедитесь, что переменная инициализирована (без получения блокировки). Если он инициализирован, немедленно верните его.
- Получите замок.
- Дважды проверьте, была ли переменная уже инициализирована: если другой поток первым получил блокировку, возможно, он уже выполнил инициализацию. Если да, верните инициализированную переменную.
- В противном случае инициализируйте и верните переменную.
// Неработающая многопоточная версия // Идиома "Double-Checked Locking" class Foo { private Helper helper ; общедоступный помощник getHelper () { если ( помощник == null ) { синхронизирован ( это ) { если ( помощник == null ) { помощник = новый помощник (); } } } return helper ; } // другие функции и члены ... }
Интуитивно этот алгоритм кажется эффективным решением проблемы. Однако у этого метода есть много тонких проблем, и его обычно следует избегать. Например, рассмотрим следующую последовательность событий:
- Поток A замечает, что значение не инициализировано, поэтому он получает блокировку и начинает инициализировать значение.
- Из-за семантики некоторых языков программирования код, сгенерированный компилятором, может обновлять общую переменную, чтобы указать на частично построенный объект, прежде чем A завершит выполнение инициализации. Например, в Java, если вызов конструктора был встроен, общая переменная может быть обновлена сразу после выделения памяти, но до того, как встроенный конструктор инициализирует объект. [6]
- Поток B замечает, что общая переменная была инициализирована (или так кажется), и возвращает ее значение. Поскольку поток B считает, что значение уже инициализировано, он не получает блокировку. Если B использует объект до того, как вся инициализация, выполненная A, будет видна B (либо потому, что A не завершил его инициализацию, либо потому, что некоторые из инициализированных значений в объекте еще не просочились в память, которую использует B ( согласованность кеша )) , скорее всего произойдет сбой программы.
Одна из опасностей использования блокировки с двойной проверкой в J2SE 1.4 (и более ранних версиях) заключается в том, что она часто будет работать: нелегко отличить правильную реализацию техники от той, в которой есть тонкие проблемы. В зависимости от компилятора , чередования потоков планировщиком и характера другой параллельной системной активности сбои в результате неправильной реализации блокировки с двойной проверкой могут происходить только периодически. Воспроизведение неудач может быть трудным.
В J2SE 5.0 эта проблема исправлена. Летучее ключевое слово в настоящее время гарантирует , что несколько потоков обработке экземпляра одноплодного правильно. Эта новая идиома описана в [3] и [4] .
// Работает с ACQuire семантикой / освобождения летучего в Java 1.5 , а затем // Разбитый под Java 1.4 и более ранней версии семантики для летучего класса Foo { частных летучего Helper помощника ; общедоступный помощник getHelper () { помощник localRef = помощник ; если ( localRef == null ) { синхронизировано ( это ) { localRef = помощник ; if ( localRef == null ) { помощник = localRef = новый помощник (); } } } return localRef ; } // другие функции и члены ... }
Обратите внимание на локальную переменную localRef , которая кажется ненужной. Результатом этого является то, что в случаях, когда помощник уже инициализирован (т. Е. Большую часть времени), к полю volatile обращаются только один раз (из-за « return localRef; » вместо « return helper; »), что может улучшить общая производительность метода на целых 40 процентов. [7]
В Java 9 был представлен VarHandle
класс, который позволяет использовать расслабленную атомику для доступа к полям, обеспечивая несколько более быстрое чтение на машинах со слабыми моделями памяти за счет более сложной механики и потери последовательной согласованности (доступ к полям больше не участвует в порядке синхронизации, глобальный порядок доступа к изменчивым полям). [8]
// Работает с семантикой получения / выпуска для VarHandles, представленных в Java 9 class Foo { private volatile Helper helper ; общедоступный помощник getHelper () { Helper localRef = getHelperAcquire (); если ( localRef == null ) { синхронизировано ( это ) { localRef = getHelperAcquire (); если ( localRef == null ) { localRef = новый помощник (); setHelperRelease ( localRef ); } } } return localRef ; } частный статический финал VarHandle HELPER ; частный помощник getHelperAcquire () { return ( Helper ) HELPER . getAcquire ( это ); } private void setHelperRelease ( значение помощника ) { HELPER . setRelease ( это , значение ); } static { попробуйте { MethodHandles . Lookup lookup = MethodHandles . поиск (); HELPER = поиск . findVarHandle ( Foo . класс , "помощник" , Helper . класс ); } catch ( ReflectiveOperationException e ) { выбросить новый ExceptionInInitializerError ( e ); } } // другие функции и члены ... }
Если вспомогательный объект является статическим (по одному на загрузчик классов), альтернативой является идиома инициализации по требованию держателя [9] (см. Листинг 16.6 [10] из ранее процитированного текста).
// Правильная отложенная инициализация в Java- классе Foo { частный статический класс HelperHolder { public static final Helper helper = new Helper (); } public static Helper getHelper () { return HelperHolder . помощник ; } }
Это основано на том факте, что вложенные классы не загружаются, пока на них нет ссылки.
Семантика последнего поля в Java 5 может использоваться для безопасной публикации вспомогательного объекта без использования volatile : [11]
открытый класс FinalWrapper < T > { общедоступное конечное значение T ; общедоступный FinalWrapper ( значение T ) { this . значение = значение ; } } открытый класс Foo { частный FinalWrapper < Helper > helperWrapper ; общедоступный помощник getHelper () { FinalWrapper < Helper > tempWrapper = helperWrapper ; if ( tempWrapper == null ) { синхронизировано ( это ) { if ( helperWrapper == null ) { helperWrapper = new FinalWrapper < Helper > ( new Helper ()); } tempWrapper = helperWrapper ; } } вернуть tempWrapper . значение ; } }
Для корректности требуется локальная переменная tempWrapper : простое использование helperWrapper как для нулевых проверок, так и для оператора return может завершиться ошибкой из-за переупорядочения чтения, разрешенного в модели памяти Java. [12] Производительность этой реализации не обязательно лучше, чем у изменчивой реализации.
Использование в C # [ править ]
Блокировка с двойной проверкой может быть эффективно реализована в .NET. Распространенным шаблоном использования является добавление блокировки с двойной проверкой к реализациям Singleton:
открытый класс MySingleton { частный статический объект _myLock = новый объект (); частный статический MySingleton _mySingleton = null ; private MySingleton () { } public static MySingleton GetInstance () { if ( _mySingleton == null ) // Первая проверка { lock ( _myLock ) { if ( _mySingleton == null ) // Вторая (двойная) проверка { _mySingleton = new MySingleton (); } } } return mySingleton ; } }
В этом примере «подсказка блокировки» - это объект mySingleton, который больше не имеет значения NULL, когда он полностью построен и готов к использованию.
В .NET Framework 4.0 Lazy<T>
был представлен класс, который по умолчанию использует блокировку с двойной проверкой (режим ExecutionAndPublication) для хранения либо исключения, которое было сгенерировано во время построения, либо результата функции, переданной в Lazy<T>
: [13]
открытый класс MySingleton { частный статический только для чтения Lazy < MySingleton > _mySingleton = new Lazy < MySingleton > (() => новый MySingleton ()); private MySingleton () { } общедоступный статический экземпляр MySingleton => _mySingleton . Стоимость ; }
См. Также [ править ]
- Испытания и испытания , и посаженные идиомы для запирающего механизма низкого уровня.
- Идиома держателя инициализации по требованию для поточно-ориентированной замены в Java.
Ссылки [ править ]
- ^ Шмидт, Д. и др. Шаблонно-ориентированная архитектура программного обеспечения Том 2, 2000, стр. 353-363
- ^ а б Дэвид Бэкон и др. Декларация «Двойная проверка блокировки нарушена» .
- ^ «Поддержка функций C ++ 11-14-17 (современный C ++)» .
- ^ Двойная проверка блокировки исправлена в C ++ 11
- Перейти ↑ Boehm, Hans-J (июнь 2005 г.). «Потоки не могут быть реализованы в виде библиотеки» (PDF) . Уведомления ACM SIGPLAN . 40 (6): 261–268. DOI : 10.1145 / 1064978.1065042 .
- ↑ Хаггар, Питер (1 мая 2002 г.). «Двойная проверка блокировки и шаблон Singleton» . IBM.
- ^ Джошуа Блох "Эффективная Java, третье издание", стр. 372
- ^ «Глава 17. Потоки и замки» . docs.oracle.com . Проверено 28 июля 2018 .
- ^ Брайан Гетц и др. Параллелизм в Java на практике, 2006, стр. 348
- ^ Гетц, Брайан; и другие. «Java Concurrency на практике - списки на сайте» . Проверено 21 октября 2014 года .
- ^ [1] Список рассылки обсуждений Javamemorymodel
- ^ [2] Мэнсон, Джереми (2008-12-14). «Ленивая инициализация Date-Race-Ful для повышения производительности - параллелизм Java (и т. Д.)» . Проверено 3 декабря +2016 .
- ^ Альбахари, Джозеф (2010). «Потоки в C #: Использование потоков» . C # 4.0 в двух словах . O'Reilly Media. ISBN 978-0-596-80095-6.
Lazy<T>
фактически реализует […] блокировку с двойной проверкой. Блокировка с двойной проверкой выполняет дополнительное энергозависимое чтение, чтобы избежать затрат на получение блокировки, если объект уже инициализирован.
Внешние ссылки [ править ]
- Проблемы с механизмом блокировки с двойной проверкой, зафиксированные в блогах Jeu George
- Описание "Double Checked Locking" из репозитория портлендских шаблонов
- "Двойная проверка блокировки нарушена" Описание из репозитория портлендских шаблонов
- Статья Скотта Мейерса и Андрея Александреску « C ++ и опасности двойной проверки блокировки » (475 КБ)
- Статья Брайана Гетца « Двойная проверка блокировки: умно, но сломано ».
- Статья Аллена Холуба « Предупреждение! Многопоточность в многопроцессорном мире »
- Двойная проверка блокировки и шаблон Singleton
- Шаблон Singleton и безопасность потоков
- ключевое слово volatile в VC ++ 2005
- Примеры Java и выбор времени для решений двойной проверки блокировки
- «Более эффективная Java с Джошуа Блохом от Google» .