﻿# V3054\. Potentially unsafe double\-checked locking\. Use volatile variable\(s\) or synchronization primitives to avoid this\.

Анализатор обнаружил потенциальную ошибку, связанную с небезопасным использованием шаблона "блокировки с двойной проверкой" \(double checked locking\)\. Блокировка с двойной проверкой \- это шаблон, предназначенный для уменьшения накладных расходов получения блокировки\. Сначала проверяется условие блокировки без синхронизации\. И только если условие выполняется, поток попытается получить блокировку\. Таким образом, блокировка будет выполнена только если она действительно была необходима\.

Рассмотрим пример небезопасной реализации данного шаблона на языке C\#:

```cpp
private static MyClass _singleton = null;
public static MyClass Singleton
{
  get
  {
    if(_singleton == null)
      lock(_locker)
      {
        if(_singleton == null)
        {
          MyClass instance = new MyClass();
          instance.Initialize();
          _singleton = instance;
        }
      }
    return _singleton;
  }
}
```

В данном примере шаблон используется для реализации "ленивой инициализации" \- инициализация откладывается до тех пор, пока значение переменной не понадобится\. Данный код будет корректно работать в программе, использующей объект '\_singleton' из одного потока\. Для обеспечения безопасной инициализации в многопоточной программе обычно используется конструкция 'lock', однако в нашем примере этого оказывается недостаточно\. 

Обратите внимание на вызов метода 'Initialize\(\)' у объекта 'Instance'\. В Release версии программы, компилятор может оптимизировать данный код и порядок назначения переменной '\_singleton' и метода 'Initialize\(\)' могут поменяться\. Таким образом, другой поток, обратившись к 'Singleton' одновременно с инициализирующим потоком, может получить доступ к объекту до того, как инициализация будет завершена\.

Рассмотрим другой пример использования шаблона блокировки с двойной проверкой:

```cpp
private static MyClass _singleton = null;
private static bool _initialized = false;
public static MyClass Singleton;
{
  get
  {
    if(!_initialized)
      lock(_locker)
      {
        if(!_initialized)
        {
          _singleton = new MyClass();
          _initialized = true;
        }
      }
    return _singleton;
  }
}
```

Мы видим, что, как и в предыдущем примере, оптимизация компилятором порядка назначений переменных '\_singleton' и '\_initialized' может привести к ошибке\. Т\.е\. в начале переменной '\_initialized' будет присвоено значение 'true', а уже потом создастся новый объект типа 'MyClass' и ссылка не него будет записана в '\_singleton'\.

Такая перестановка может привести к ошибке при доступе к объекту из параллельного потока\. Получается, что переменная '\_singleton' будет ещё не назначена, а флаг '\_intialize' уже будет выставлен в 'true'\.

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

Есть несколько способов обеспечить потоко\-безопасность для данного шаблона\. Самым простым будет пометить проверяемую в условии if переменную ключевым словом volatile:

```cpp
private static volatile MyClass _singleton = null;
public static MyClass Singleton
{
  get
  {
    if(_singleton == null)
      lock(_locker)
      {
        if(_singleton == null)
        {
          MyClass instance = new MyClass();
          instance.Initialize();
          _singleton = instance;
        }
      }
    return _singleton;
  }
}
```

Использование ключевого слова 'volatile' предотвратит для переменной возможные оптимизации компилятора, связанные с перестановками инструкций записи\\чтения и кэшированием её значения в регистрах процессора\. 

Из соображений производительности не всегда желательно объявлять переменную как 'volatile'\. В этом случае можно организовать доступ к переменной с помощью методов: 'Thread\.VolatileRead', 'Thread\.VolatileWrite' и 'Thread\.MemoryBarrier'\. Эти методы создадут барьеры по чтению\\записи памяти только там, где это необходимо\.

Наконец, для реализации "ленивой инициализации" можно воспользоваться специально предназначенным для этого классом 'Lazy<T\>', доступным начиная с \.NET 4\.

См\. также статью: [Выявление неправильной блокировки с двойной проверкой с помощью диагностики V3054](https://pvs-studio.ru/ru/blog/posts/csharp/0715/)\.