Jump to content

Двойная проверка блокировки

В обеспечения разработке программного блокировка с двойной проверкой (также известная как «оптимизация блокировки с двойной проверкой»). [1] ) — это шаблон проектирования программного обеспечения, используемый для снижения затрат на получение блокировки путем проверки критерия блокировки («подсказка блокировки») перед получением блокировки. Блокировка происходит только в том случае, если проверка критерия блокировки показывает, что блокировка требуется.

Исходная форма шаблона, появляющаяся в «Языках шаблонов проектирования программ 3» , [2] имеет гонки данных , в зависимости от используемой модели памяти , и это трудно сделать правильно. Некоторые считают это антипаттерном . [3] Существуют допустимые формы шаблона, включая использование volatile ключевое слово в Java и явные барьеры памяти в C++. [4]

Этот шаблон обычно используется для уменьшения накладных расходов на блокировку при реализации « ленивой инициализации » в многопоточной среде, особенно как часть шаблона Singleton . Ленивая инициализация позволяет избежать инициализации значения до первого доступа к нему.

Мотивация и оригинальный узор

[ редактировать ]

Рассмотрим, например, этот сегмент кода на языке программирования Java : [4]

// Single-threaded version
class Foo {
    private static Helper helper;
    public Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }

    // other functions and members...
}

Проблема в том, что это не работает при использовании нескольких потоков. Блокировка вызывают должна быть получена в случае, если два потока getHelper() одновременно. В противном случае либо они оба попытаются создать объект одновременно, либо кто-то может получить ссылку на не полностью инициализированный объект.

Синхронизация с блокировкой может исправить это, как показано в следующем примере:

// Correct but possibly expensive multithreaded version
class Foo {
    private Helper helper;
    public synchronized Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }

    // other functions and members...
}

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

  1. Убедитесь, что переменная инициализирована (без получения блокировки). Если он инициализирован, немедленно верните его.
  2. Получите замок.
  3. Дважды проверьте, была ли уже инициализирована переменная: если другой поток первым получил блокировку, возможно, он уже выполнил инициализацию. Если да, верните инициализированную переменную.
  4. В противном случае инициализируйте и верните переменную.
// Broken multithreaded version
// original "Double-Checked Locking" idiom
class Foo {
    private Helper helper;
    public Helper getHelper() {
        if (helper == null) {
            synchronized (this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }

    // other functions and members...
}

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

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

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

Использование в C++11

[ редактировать ]

Для шаблона Singleton блокировка с двойной проверкой не требуется:

Если элемент управления входит в объявление одновременно с инициализацией переменной, параллельное выполнение должно дождаться завершения инициализации.

§ 6.7 [stmt.dcl] стр. 4
Singleton& GetInstance() {
  static Singleton s;
  return s;
}

C++11 и более поздние версии также предоставляют встроенный шаблон блокировки с двойной проверкой в ​​виде std::once_flag и std::call_once:

#include <mutex>
#include <optional> // Since C++17

// Singleton.h
class Singleton {
 public:
  static Singleton* GetInstance();
 private:
  Singleton() = default;

  static std::optional<Singleton> s_instance;
  static std::once_flag s_flag;
};

// Singleton.cpp
std::optional<Singleton> Singleton::s_instance;
std::once_flag Singleton::s_flag{};

Singleton* Singleton::GetInstance() {
  std::call_once(Singleton::s_flag,
                 []() { s_instance.emplace(Singleton{}); });
  return &*s_instance;
}

Если кто-то действительно желает использовать идиому с двойной проверкой вместо тривиально работающего примера, приведенного выше (например, потому, что Visual Studio до выпуска 2015 года не реализовывал язык стандарта C++ 11 о параллельной инициализации, указанный выше [7] ), необходимо использовать заборы захвата и освобождения: [8]

#include <atomic>
#include <mutex>

class Singleton {
 public:
  static Singleton* GetInstance();

 private:
  Singleton() = default;

  static std::atomic<Singleton*> s_instance;
  static std::mutex s_mutex;
};

Singleton* Singleton::GetInstance() {
  Singleton* p = s_instance.load(std::memory_order_acquire);
  if (p == nullptr) { // 1st check
    std::lock_guard<std::mutex> lock(s_mutex);
    p = s_instance.load(std::memory_order_relaxed);
    if (p == nullptr) { // 2nd (double) check
      p = new Singleton();
      s_instance.store(p, std::memory_order_release);
    }
  }
  return p;
}

Использование в POSIX

[ редактировать ]

pthread_once() необходимо использовать для инициализации кода библиотеки (или подмодуля), когда ее API не имеет специальной инициализации процедуру необходимо вызывать в однопоточном режиме.

Использование в Go

[ редактировать ]
package main

import "sync"

var arrOnce sync.Once
var arr []int

// getArr retrieves arr, lazily initializing on first call. Double-checked
// locking is implemented with the sync.Once library function. The first
// goroutine to win the race to call Do() will initialize the array, while
// others will block until Do() has completed. After Do has run, only a
// single atomic comparison will be required to get the array.
func getArr() []int {
	arrOnce.Do(func() {
		arr = []int{0, 1, 2}
	})
	return arr
}

func main() {
	// thanks to double-checked locking, two goroutines attempting to getArr()
	// will not cause double-initialization
	go getArr()
	go getArr()
}

Использование в Java

[ редактировать ]

Начиная с J2SE 5.0 , ключевое слово Летучие определено для создания барьера памяти. Это позволяет найти решение, гарантирующее, что несколько потоков правильно обрабатывают одноэлементный экземпляр. Эта новая идиома описана в [3] и [4] .

// Works with acquire/release semantics for volatile in Java 1.5 and later
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper;
    public Helper getHelper() {
        Helper localRef = helper;
        if (localRef == null) {
            synchronized (this) {
                localRef = helper;
                if (localRef == null) {
                    helper = localRef = new Helper();
                }
            }
        }
        return localRef;
    }

    // other functions and members...
}

Обратите внимание на локальную переменную " localRef ", что кажется ненужным. Результатом этого является то, что в тех случаях, когда помощник уже инициализирован (т.е. большую часть времени), доступ к изменчивому полю осуществляется только один раз (из-за " вернуть локальную ссылку; " вместо " возвратный помощник; "), что может улучшить общую производительность метода на целых 40 процентов. [9]

В Java 9 появилась VarHandle класс, который позволяет использовать расслабленную атомарность для доступа к полям, обеспечивая несколько более быстрое чтение на машинах со слабыми моделями памяти, за счет более сложной механики и потери последовательной согласованности (доступ к полям больше не участвует в порядке синхронизации, глобальном порядке доступ к изменчивым полям). [10]

// Works with acquire/release semantics for VarHandles introduced in Java 9
class Foo {
    private volatile Helper helper;

    public Helper getHelper() {
        Helper localRef = getHelperAcquire();
        if (localRef == null) {
            synchronized (this) {
                localRef = getHelperAcquire();
                if (localRef == null) {
                    localRef = new Helper();
                    setHelperRelease(localRef);
                }
            }
        }
        return localRef;
    }

    private static final VarHandle HELPER;
    private Helper getHelperAcquire() {
        return (Helper) HELPER.getAcquire(this);
    }
    private void setHelperRelease(Helper value) {
        HELPER.setRelease(this, value);
    }

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            HELPER = lookup.findVarHandle(Foo.class, "helper", Helper.class);
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    // other functions and members...
}

Если вспомогательный объект является статическим (по одному на каждый загрузчик классов), альтернативой является идиома держателя инициализации по требованию. [11] (См. листинг 16.6. [12] из ранее цитированного текста.)

// Correct lazy initialization in Java
class Foo {
    private static class HelperHolder {
       public static final Helper helper = new Helper();
    }

    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}

Это основано на том факте, что вложенные классы не загружаются до тех пор, пока на них не будет сделана ссылка.

Семантика Поле Final в Java 5 можно использовать для безопасной публикации вспомогательного объекта без использования изменчивый : [13]

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class Foo {
   private FinalWrapper<Helper> helperWrapper;

   public Helper getHelper() {
      FinalWrapper<Helper> tempWrapper = helperWrapper;

      if (tempWrapper == null) {
          synchronized (this) {
              if (helperWrapper == null) {
                  helperWrapper = new FinalWrapper<Helper>(new Helper());
              }
              tempWrapper = helperWrapper;
          }
      }
      return tempWrapper.value;
   }
}

Локальная переменная tempWrapper необходим для корректности: просто используя helperWrapper как для проверки null, так и для оператора return может завершиться неудачей из-за переупорядочения чтения, разрешенного в модели памяти Java. [14] Производительность этой реализации не обязательно лучше, чем у нестабильная реализация.

Использование в C#

[ редактировать ]

В .NET Framework 4.0 Lazy<T> был введен класс, который по умолчанию использует внутреннюю блокировку с двойной проверкой (режим ExecutionAndPublication) для хранения либо исключения, которое было создано во время построения, либо результата функции, которая была передана в Lazy<T>: [15]

public class MySingleton
{
    private static readonly Lazy<MySingleton> _mySingleton = new Lazy<MySingleton>(() => new MySingleton());

    private MySingleton() { }

    public static MySingleton Instance => _mySingleton.Value;
}

См. также

[ редактировать ]
  1. ^ Шмидт, Д. и др. Архитектура программного обеспечения, ориентированная на шаблоны, том 2, 2000, стр. 353-363.
  2. ^ Языки шаблонов проектирования программ. 3 (PDF) (Начдр. ред.). Ридинг, Массачусетс: Аддисон-Уэсли. 1998. ISBN  978-0201310115 .
  3. ^ Грегуар, Марк (24 февраля 2021 г.). Профессиональный С++ . Джон Уайли и сыновья. ISBN  978-1-119-69545-5 .
  4. ^ Перейти обратно: а б Дэвид Бэкон и др. Декларация «Двойная проверка блокировки нарушена» .
  5. ^ Бём, Ханс-Дж (июнь 2005 г.). «Потоки не могут быть реализованы как библиотека» (PDF) . Уведомления ACM SIGPLAN . 40 (6): 261–268. дои : 10.1145/1064978.1065042 . Архивировано из оригинала (PDF) 30 мая 2017 г. Проверено 12 августа 2014 г.
  6. ^ Хаггар, Питер (1 мая 2002 г.). «Блокировка с двойной проверкой и шаблон Singleton» . ИБМ. Архивировано из оригинала 27 октября 2017 г. Проверено 19 мая 2022 г.
  7. ^ «Поддержка функций C++11-14-17 (современный C++)» .
  8. ^ Блокировка с двойной проверкой исправлена ​​в C++11.
  9. ^ Блох, Джошуа (2018). Эффективная Java (Третье изд.). Аддисон-Уэсли. п. 335. ИСБН  978-0-13-468599-1 . На моей машине описанный выше метод примерно в 1,4 раза быстрее, чем очевидная версия без локальной переменной.
  10. ^ «Глава 17. Нитки и замки» . docs.oracle.com . Проверено 28 июля 2018 г.
  11. ^ Брайан Гетц и др. Параллелизм Java на практике, 2006, стр. 348
  12. ^ Гетц, Брайан; и др. «Параллелизм Java на практике – списки на веб-сайте» . Проверено 21 октября 2014 г.
  13. ^ [1] Список рассылки для обсуждения модели памяти Javamemory
  14. ^ [2] Мэнсон, Джереми (14 декабря 2008 г.). «Отложенная инициализация с гонкой по датам для повышения производительности — параллелизм Java (&c)» . Проверено 3 декабря 2016 г.
  15. ^ Альбахари, Джозеф (2010). «Поточность в C#: использование потоков» . Кратко о C# 4.0 . О'Рейли Медиа. ISBN  978-0-596-80095-6 . Lazy<T> на самом деле реализует […] блокировку с двойной проверкой. Блокировка с двойной проверкой выполняет дополнительное энергозависимое чтение, чтобы избежать затрат на получение блокировки, если объект уже инициализирован.
[ редактировать ]
Arc.Ask3.Ru: конец переведенного документа.
Arc.Ask3.Ru
Номер скриншота №: 416828f3e17d7881e8cb35b4f9231e36__1698618540
URL1:https://arc.ask3.ru/arc/aa/41/36/416828f3e17d7881e8cb35b4f9231e36.html
Заголовок, (Title) документа по адресу, URL1:
Double-checked locking - Wikipedia
Данный printscreen веб страницы (снимок веб страницы, скриншот веб страницы), визуально-программная копия документа расположенного по адресу URL1 и сохраненная в файл, имеет: квалифицированную, усовершенствованную (подтверждены: метки времени, валидность сертификата), открепленную ЭЦП (приложена к данному файлу), что может быть использовано для подтверждения содержания и факта существования документа в этот момент времени. Права на данный скриншот принадлежат администрации Ask3.ru, использование в качестве доказательства только с письменного разрешения правообладателя скриншота. Администрация Ask3.ru не несет ответственности за информацию размещенную на данном скриншоте. Права на прочие зарегистрированные элементы любого права, изображенные на снимках принадлежат их владельцам. Качество перевода предоставляется как есть. Любые претензии, иски не могут быть предъявлены. Если вы не согласны с любым пунктом перечисленным выше, вы не можете использовать данный сайт и информация размещенную на нем (сайте/странице), немедленно покиньте данный сайт. В случае нарушения любого пункта перечисленного выше, штраф 55! (Пятьдесят пять факториал, Денежную единицу (имеющую самостоятельную стоимость) можете выбрать самостоятельно, выплаичвается товарами в течение 7 дней с момента нарушения.)