>
>
>
V3090. Unsafe locking on an object.


V3090. Unsafe locking on an object.

Анализатор обнаружил фрагмент кода с использованием небезопасной блокировки.

Данная диагностика срабатывает на ряд случаев:

  • использование 'this' в качестве объекта блокировки;
  • использование в качестве объекта блокировки экземпляра класса 'Type', 'MemberInfo', 'ParameterInfo', 'String', 'Thread';
  • использование в качестве объекта блокировки публичного члена текущего класса;
  • использование в качестве объекта блокировки объекта, полученного в результате упаковки;
  • блокировка происходит по вновь создаваемым объектам.

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

Блокировка с использованием 'this' небезопасна, если класс не является приватным. Тогда в другом месте программист может установить блокировку на этот же объект после создания его экземпляра.

Аналогичная ситуация с публичными членами классов.

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

Приведём пример небезопасного кода с использованием оператора 'lock' и 'this' в качестве объекта блокировки:

class A
{
  void Foo()
  {
    lock(this)
    {
      // do smt
    }
  }
}

Для того, чтобы избежать возможных взаимных блокировок, в качестве объекта блокировки стоит использовать, например, приватное поле:

class A
{
  private Object locker = new Object();
  void Foo()
  {
    lock(locker)
    {
      // do smt
    }
  }
}

С экземплярами класса 'Type', 'MemberInfo', 'ParameterInfo' ситуация несколько опаснее, так как здесь вероятность взаимной блокировки выше. Используя оператор 'typeof' или метод 'GetType', метод 'GetMember' и т.п. для разных экземпляров одного типа, результат будет одинаков - один и тот же экземпляр данного класса.

Отдельно стоят объекты типов 'String' и 'Thread'.

Доступ к объектам этих типов можно получить из любого места в программе, и даже из другого домена приложения (Application Domain), что ещё больше увеличивает опасность взаимной блокировки. Решение - не использовать в качестве объектов блокировки экземпляры этих типов.

Рассмотрим пример возникновения взаимной блокировки. Пусть имеется следующий код в каком-нибудь приложении (Sample.exe):

static void Main(string[] args)
{
  var thread = new Thread(() => Process());
  thread.Start();
  thread.Join();
}
static void Process()
{
  String locker = "my locker";
  lock (locker)
  { 
    ....
  }
}

В другом приложении есть код следующего вида:

String locker = "my locker";
lock (locker)
{
  AppDomain domain = AppDomain.CreateDomain("test");
  domain.ExecuteAssembly(@"C:\Sample.exe");
}

Результатом выполнения этого кода будет взаимная блокировка, возникшая в результате использования в качестве объекта блокировки экземпляра класса 'String'.

Мы создаём новый домен в рамках того же процесса и пытаемся выполнить в нём сборку, содержащуюся в другом файле (Sample.exe). В итоге возникает ситуация, когда оба оператора 'lock' используют в качестве объекта блокировки одинаковый строковый литерал. Для строковых литералов срабатывает механизм интернирования строк, за счёт которого в обоих случаях будут получены ссылки на один и тот же объект в памяти. Как следствие - оба оператора 'lock' выполняют блокировку по одному и тому же объекту, что и привело к взаимоблокировке.

Эта ошибка могла бы проявить себя и при исполнении в рамках одного домена.

Аналогичная ситуация с типом 'Thread', экземпляр которого можно легко получить, например, с помощью свойства 'Thread.CurrentThread'.

Решение - не использовать в качестве объектов блокировки объекты типов 'Thread' и 'String'.

Блокировка с использованием объекта значимого типа приведёт к тому, что синхронизация потоков осуществляться не будет. Стоит отметить, что конструкция 'lock' не позволяет выполнять блокировку на объектах значимого типа, но класс 'Monitor' с его методами 'Enter' и 'TryEnter' от этого не застрахован.

Методы 'Enter' и 'TryEnter' ожидают в качестве параметра объект типа 'Object', поэтому, если в метод передаётся объект значимого типа, будет выполнена его 'упаковка'. Это значит, что каждый раз для блокировки будет создаваться новый объект, следовательно - блокировка будет устанавливаться (и сниматься) по этому новому объекту. Результат - отсутствие желаемой синхронизации.

Рассмотрим пример ошибочного кода:

sealed class A
{
  private Int32 m_locker = 10;

  void Foo()
  {
    Monitor.Enter(m_locker);
    // Do smt...
    Monitor.Exit(m_locker);
  }
}

Программист хотел установить блокировку по приватному полю 'm_locker'. На самом же деле блокировка будет устанавливаться (и сниматься) не по желаемому полю, а по вновь созданным объектам, полученным в результате 'упаковки'.

Для исправления данной ошибки достаточно изменить тип поля 'm_locker' на допустимый ссылочный, например - 'Object'. Тогда пример корректного кода выглядел бы так:

sealed class A
{
  private Object m_locker = new Object();

  void Foo()
  {
    Monitor.Enter(m_locker);
    // Do smt...
    Monitor.Exit(m_locker);
  }
}

Схожая ошибка проявится и при использовании конструкции 'lock', если выполняется упаковка объекта в результате приведения:

Int32 val = 10;
lock ((Object)val)
{ .... }

В этом коде блокировка будет устанавливаться по объектам, полученным в результате упаковки. Так как в результате упаковки будут создаваться новые объекты, синхронизации потоков не будет.

Ошибочным является блокировка по вновь создаваемым объектам. Пример подобного кода может выглядеть так:

lock (new Object())
{ .... }

или так

lock (obj = new Object())
{ .... }

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

Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.).

Данная диагностика классифицируется как:

Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3090.