Блокировка с двойной проверкой
Блокировка с двойной проверкой (англ. double-checked locking) — параллельный шаблон проектирования, предназначающийся для уменьшения накладных расходов, связанных с получением блокировки. Сначала проверяется условие блокировки без какой-либо синхронизации; поток делает попытку получить блокировку, только если результат проверки говорит о том, что получение блокировки необходимо. На некоторых языках и/или на некоторых машинах невозможно безопасно реализовать данный шаблон. Поэтому иногда его называют анти-паттерном. Такие особенности привели к появлению отношения строгого порядка «happens before» в Java Memory Model и C++ Memory Model. Обычно он используется для уменьшения накладных расходов при реализации ленивой инициализации в многопоточных программах, например в составе шаблона проектирования Одиночка. При ленивой инициализации переменной, инициализация откладывается до тех пор, пока значение переменной не понадобится при вычислениях. Пример использования в JavaРассмотрим следующий код на языке Java, взятый из[1]: // Однопоточная версия
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
// и остальные члены класса…
}
Этот код не будет корректно работать в многопоточной программе. Метод // Правильная, но "дорогая" по времени выполнения многопоточная версия
class Foo {
private Helper helper = null;
public synchronized Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
// и остальные члены класса…
}
Этот код работает, но он вносит дополнительные накладные расходы на синхронизацию. Первый вызов
// Некорректно работающая (в Symantec JIT и версиях Java 1.4 и ранее) многопоточная версия
// Шаблон "Double-Checked Locking"
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
// и остальные члены класса…
}
На интуитивном уровне этот код кажется корректным. Однако существуют некоторые проблемы (в версии Java 1.4 и ранее и нестандартных реализациях JRE), которых, возможно, следует избегать. Представим себе, что события в многопоточной программе протекают так:
Одна из опасностей использования блокировки с двойной проверкой в J2SE 1.4 (и более ранних версиях) состоит в том, что часто кажется, что программа работает корректно. Во-первых, рассмотренная ситуация будет возникать не очень часто; во-вторых, сложно отличить корректную реализацию данного шаблона от такой, которая имеет описанную проблему. В зависимости от компилятора, распределения планировщиком процессорного времени для потоков, а также природы других работающих конкурентных процессов, ошибки, спровоцированные с некорректной реализацией блокировки с двойной проверкой, обычно происходят бессистемно. Воспроизведение таких ошибок обычно затруднено. Можно решить проблему при использовании J2SE 5.0. Новая семантика ключевого слова // Работает с новой семантикой volatile
// Не работает в Java 1.4 и более ранних версиях из-за семантики volatile
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
// и остальные члены класса…
}
Было предложено много вариантов блокировки с двойной проверкой, которые явно (при помощи volatile или синхронизации) не сообщают то, что объект полностью сконструирован, и все они являются некорректными для Symantec JIT и для устаревших версиях Oracle JRE [2][3]. Пример использования в C#public sealed class Singleton
{
private Singleton()
{
// инициализировать новый экземпляр объекта
}
private static volatile Singleton singletonInstance;
private static readonly Object syncRoot = new Object();
public static Singleton GetInstance()
{
// создан ли объект
if(singletonInstance == null)
{
// нет, не создан
// только один поток может создать его
lock(syncRoot)
{
// проверяем, не создал ли объект другой поток
if(singletonInstance == null)
{
// нет не создал — создаём
singletonInstance = new Singleton();
}
}
}
return singletonInstance;
}
}
Microsoft подтверждает [4], что при использовании ключевого слова volatile, использование паттерна Double checked locking является безопасным. Пример использования в PythonСледующий код на языке Python показывает пример реализации отложенной инициализации в сочетании с шаблоном Double checked locking: # require Python2 or Python3
#-*- coding: UTF-8 *-*
import threading
class SimpleLazyProxy:
'''ленивая инициализация объекта
безопасная для многонитевого использования'''
def __init__(self, factory):
self.__lock = threading.RLock()
self.__obj = None
self.__factory = factory
def __call__(self):
'''функция для доступа к настоящему объекту
если объект не создан, то он создастся'''
# пробуем получить "быстрый" доступ к объекту:
obj = self.__obj
if obj is not None:
# получилось!
return obj
else:
# объект, возможно, ещё не создан
with self.__lock:
# получаем доступ к объекту в эксклюзивном режиме:
obj = self.__obj
if obj is not None:
# оказалось, объект уже создан.
# не будем повторно его создавать
return obj
else:
# объект действительно ещё не создан.
# создадим же его!
obj = self.__factory()
self.__obj = obj
return obj
__getattr__ = lambda self, name: \
getattr(self(), name)
def lazy(proxy_cls=SimpleLazyProxy):
'''декоратор, превращающий класс в класс с ленивой инициализацией
средствами Proxy-класса'''
class ClassDecorator:
def __init__(self, cls):
# инициализация декоратора,
# но не декорируемого класса и не Proxy-класса
self.cls = cls
def __call__(self, *args, **kwargs):
# запрос инициализации Proxy-класса
# передадим Proxy-классу нужные параметры
# для инициализации декорируемого класса
return proxy_cls(lambda: self.cls(*args, **kwargs))
return ClassDecorator
# простая проверка:
def test_0():
print('\t\t\t*** Начало теста ***')
import time
@lazy() # экземпляры этого класса будут с ленивой инициализацией
class TestType:
def __init__(self, name):
print('%s: Создаётся...' % name)
# искусственно увеличим время создания объекта
# для нагнетения конкуренции потоков
time.sleep(3)
self.name = name
print('%s: Создался!' % name)
def test(self):
print('%s: Проверка' % self.name)
# один такой экземпляр будет взаимодействовать с несколькими потоками
test_obj = TestType('Межнитевый тестовый объект')
target_event = threading.Event()
def threads_target():
# функция, которую будут выполнять потоки:
# ждём наступления специального события
target_event.wait()
# как только это событие наступит -
# все 10 потоков одновременно обратятся к тестовому объекту
# и в этот момент он инициализируется в одном из потоков
test_obj.test()
# создадим эти 10 потоков с вышеописанным алгоритмом threads_target()
threads = []
for thread in range(10):
thread = threading.Thread(target=threads_target)
thread.start()
threads.append(thread)
print('До этого момента обращений к объекту не было')
# подождём немного времени...
time.sleep(3)
# ...и запустим test_obj.test() одновременно во всех потоках
print('Активируем событие для использования тестового объекта!')
target_event.set()
# завершение
for thread in threads:
thread.join()
print('\t\t\t*** Конец теста ***')
Ссылки
Примечания
|
Portal di Ensiklopedia Dunia