Из Википедии, бесплатной энциклопедии
Перейти к навигации Перейти к поиску

В программной инженерии , перепроверил замок (также известный как «двойной проверку блокировки оптимизации» [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] накладные расходы на получение и снятие блокировки каждый раз, когда вызывается этот метод, кажутся ненужными: после завершения инициализации получение и освобождение замки казались бы ненужными. Многие программисты пытались оптимизировать эту ситуацию следующим образом:

  1. Убедитесь, что переменная инициализирована (без получения блокировки). Если он инициализирован, немедленно верните его.
  2. Получите замок.
  3. Дважды проверьте, была ли переменная уже инициализирована: если другой поток первым получил блокировку, возможно, он уже выполнил инициализацию. Если да, верните инициализированную переменную.
  4. В противном случае инициализируйте и верните переменную.
// Неработающая многопоточная версия // Идиома "Double-Checked Locking" class  Foo  {  private  Helper  helper ;  общедоступный  помощник  getHelper ()  {  если  ( помощник  ==  null )  {  синхронизирован  ( это )  {  если  ( помощник  ==  null )  {  помощник  =  новый  помощник ();  }  }  }  return  helper ;  } // другие функции и члены ... }

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

  1. Поток A замечает, что значение не инициализировано, поэтому он получает блокировку и начинает инициализировать значение.
  2. Из-за семантики некоторых языков программирования код, сгенерированный компилятором, может обновлять общую переменную, чтобы указать на частично построенный объект, прежде чем A завершит выполнение инициализации. Например, в Java, если вызов конструктора был встроен, общая переменная может быть обновлена ​​сразу после выделения памяти, но до того, как встроенный конструктор инициализирует объект. [6]
  3. Поток 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.

Ссылки [ править ]

  1. ^ Шмидт, Д. и др. Шаблонно-ориентированная архитектура программного обеспечения Том 2, 2000, стр. 353-363
  2. ^ а б Дэвид Бэкон и др. Декларация «Двойная проверка блокировки нарушена» .
  3. ^ «Поддержка функций C ++ 11-14-17 (современный C ++)» .
  4. ^ Двойная проверка блокировки исправлена ​​в C ++ 11
  5. Перейти ↑ Boehm, Hans-J (июнь 2005 г.). «Потоки не могут быть реализованы в виде библиотеки» (PDF) . Уведомления ACM SIGPLAN . 40 (6): 261–268. DOI : 10.1145 / 1064978.1065042 .
  6. Хаггар, Питер (1 мая 2002 г.). «Двойная проверка блокировки и шаблон Singleton» . IBM.
  7. ^ Джошуа Блох "Эффективная Java, третье издание", стр. 372
  8. ^ «Глава 17. Потоки и замки» . docs.oracle.com . Проверено 28 июля 2018 .
  9. ^ Брайан Гетц и др. Параллелизм в Java на практике, 2006, стр. 348
  10. ^ Гетц, Брайан; и другие. «Java Concurrency на практике - списки на сайте» . Проверено 21 октября 2014 года .
  11. ^ [1] Список рассылки обсуждений Javamemorymodel
  12. ^ [2] Мэнсон, Джереми (2008-12-14). «Ленивая инициализация Date-Race-Ful для повышения производительности - параллелизм Java (и т. Д.)» . Проверено 3 декабря +2016 .
  13. ^ Альбахари, Джозеф (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» .