Double checked locking

Double checked locking
Шаблон проектирования
Блокировка с двойной проверкой
Double checked locking
Описан в Design Patterns

Нет

Double checked locking (блокировка с двойной проверкой) — шаблон проектирования, применяющийся в параллельном программировании. Он предназначен для уменьшения накладных расходов, связанных с получением блокировки. Сначала проверяется условие блокировки без какой-либо синхронизации; поток делает попытку получить блокировку только если результат проверки говорит о том, что ни один другой поток не владеет блокировкой.

На некоторых языках и/или на некоторых машинах невозможно безопасно реализовать данный шаблон. Поэтому иногда его называют анти-паттерном.

Обычно он используется для уменьшения накладных расходов при реализации ленивой инициализации в многопоточных программах, например в составе шаблона проектирования Одиночка. При ленивой инициализации переменной, инициализация откладывается до тех пор, пока значение переменной не понадобится при вычислениях.

Содержание

Пример использования в Java

Рассмотрим следующий код на языке Java, взятый из [1]:

// Однопоточная версия
class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null)
            helper = new Helper();
        return helper;
    }
 
    // и остальные члены класса…
}

Этот код не будет корректно работать в многопоточной программе. Метод getHelper() должен получать блокировку на случай, если его вызовут одновременно из двух потоков. Действительно, если поле helper ещё не инициализировано, и одновременно два потока вызовут метод getHelper(), то оба потока попытаются создать объект, что приведет к созданию лишнего объекта. Эта проблема решается использованием синхронизации, как показано в следующем примере.

// Правильная, но "дорогая" по времени выполнения многопоточная версия
class Foo { 
    private Helper helper = null;
    public synchronized Helper getHelper() {
        if (helper == null)
            helper = new Helper();
        return helper;
    }
 
    // и остальные члены класса…
}

Этот код работает, но он вносит дополнительные накладные расходы на синхронизацию. Первый вызов getHelper() создаст объект, и нужно синхронизировать только те несколько потоков, которые будут вызывать getHelper() во время инициализации объекта. После инициализации синхронизация при вызове getHelper() является излишней, так как будет производиться только чтение переменной. Так как синхронизация может уменьшить производительность в 100 раз и более, накладные расходы на блокировку при каждом вызове этого метода кажутся излишними: как только инициализация завершена, необходимость в блокировке отпадает. Многие программисты попытались оптимизировать этот код следующим образом:

  1. Сначала проверяется, инициализирована ли переменная (без получения блокировки). Если она инициализирована, её значение возвращается немедленно.
  2. Получение блокировки.
  3. Повторно проверяется, инициализирована ли переменная, так как вполне возможно, что после первой проверки другой поток инициализировал переменную. Если она инициализирована, её значение возвращается.
  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;
    }
 
    // и остальные члены класса…
}

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

  1. Поток А замечает, что переменная не инициализирована, затем получает блокировку и начинает инициализацию.
  2. Семантика некоторых языков программирования[каких?] такова, что потоку А разрешено присвоить разделяемой переменной ссылку на объект, который находится в процессе инициализации.
  3. Поток Б замечает, что переменная инициализирована (по крайней мере, ему так кажется), и возвращает значение переменной без получения блокировки. Если поток Б теперь будет использовать переменную до того момента, когда поток А закончит инициализацию, поведение программы будет некорректным.

Одна из опасностей использования блокировки с двойной проверкой в J2SE 1.4 (и более ранних версиях) состоит в том, что часто кажется, что программа работает корректно. Во-первых, рассмотренная ситуация будет возникать не очень часто; во-вторых, сложно отличить корректную реализацию данного шаблона от такой, которая имеет описанную проблему. В зависимости от компилятора, распределения планировщиком процессорного времени для потоков, а также природы других работающих конкурентных процессов, ошибки, спровоцированные с некорректной реализацией блокировки с двойной проверкой, обычно происходят бессистемно. Воспроизведение таких ошибок обычно затруднено.

Можно решить проблему при использовании J2SE 5.0. Новая семантика ключевого слова volatile даёт возможность корректно обработать запись в переменную в данном случае. Этот новый шаблон описан в [2]:

// Работает с новой семантикой 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 или синхронизации) не сообщают то, что объект полностью сконструирован, и все они являются некорректными [3][4].

Пример использования в 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 подтверждает [5], что при использовании ключевого слова 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):
                '''функция для доступа к настоящему объекту
 
                если объект не создан, то он создаcтся'''
 
                # пробуем получить "быстрый" доступ к объекту:
                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*** Конец теста ***')


Ссылки


Wikimedia Foundation. 2010.

Игры ⚽ Поможем написать реферат

Полезное


Смотреть что такое "Double checked locking" в других словарях:

  • Double checked locking — En génie logiciel, le verrouillage à double test ou double checked locking est un ancien patron de conception[1]. Considéré aujourd hui comme un antipattern du fait des problèmes subtils et difficiles à déceler qu il pose, il a été utilisé dans… …   Wikipédia en Français

  • Double-checked locking — In software engineering, double checked locking (also known as double checked locking optimization[1] .) is a software design pattern used to reduce the overhead of acquiring a lock by first testing the locking criterion (the lock hint ) without… …   Wikipedia

  • Double-checked locking — En génie logiciel, le verrouillage à double test ou double checked locking est un ancien patron de conception[1]. Considéré aujourd hui comme un antipattern du fait des problèmes subtils et difficiles à déceler qu il pose, il a été utilisé dans… …   Wikipédia en Français

  • Блокировка с двойной проверкой — Double checked locking (блокировка с двойной проверкой) шаблон проектирования применяющийся в параллельном программировании. Он предназначен для уменьшения накладных расходов, связанных с получением блокировки. Сначала проверяется условие… …   Википедия

  • Anti-pattern — (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster, Architekturmuster...),… …   Deutsch Wikipedia

  • Antimuster — Anti Pattern (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster,… …   Deutsch Wikipedia

  • Antipattern — Anti Pattern (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster,… …   Deutsch Wikipedia

  • Negativmuster — Anti Pattern (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster,… …   Deutsch Wikipedia

  • Anti-Pattern — (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster, Architekturmuster...),… …   Deutsch Wikipedia

  • Singleton pattern — In software engineering, the singleton pattern is a design pattern used to implement the mathematical concept of a singleton, by restricting the instantiation of a class to one object. This is useful when exactly one object is needed to… …   Wikipedia


Поделиться ссылкой на выделенное

Прямая ссылка:
Нажмите правой клавишей мыши и выберите «Копировать ссылку»