Анализатор обнаружил фрагмент кода с использованием небезопасной блокировки.
Данная диагностика срабатывает на ряд случаев:
Первые три случая могут привести к возникновению взаимной блокировки, два последних - к отсутствию желаемой синхронизации. Общая суть первых трёх случаев состоит в том, что к объекту, используемому для блокировки, имеется общий доступ. Такой объект может быть использован для блокировки в другом месте без ведома разработчика, использовавшего объект для блокировки в первый раз. Это, в свою очередь, создаёт вероятность возникновения взаимоблокировки на один и тот же объект.
Блокировка с использованием '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. |