Классы Interlocked и Monitor

68

Класс Interlocked

Класс Interlocked позволяет создавать простые операторы для атомарных операций с переменными. Например, операция i++ не является безопасной в отношении потоков. Она подразумевает извлечение значения из памяти, увеличение этого значения на 1 и его обратное сохранение в памяти. Такие операции могут прерываться планировщиком потоков. Класс Interlocked предоставляет методы, позволяющие выполнять инкремент, декремент, обмен и считывание значений в безопасной к потокам манере.

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

Например, вместо того чтобы применять оператор lock для блокирования доступа к переменной при установке для нее нового значения в случае, если ее текущим значением является null, можно воспользоваться классом Interlocked, что гораздо быстрее. Ниже представлены основные члены данного класса:

Член Назначение
CompareExchange() Безопасно проверяет два значения на эквивалентность. Если они эквивалентны, изменяет одно из значений на третье
Decrement() Безопасно уменьшает значение на 1
Exchange() Безопасно меняет два значения местами
Increment() Безопасно увеличивает значение на 1

Хотя это не видно сразу, процесс атомарного изменения отдельного значения довольно часто применяется в многопоточной среде. Предположим, что имеется метод по имени AddOne(), который увеличивает целочисленную переменную-член по имени intVal. Вместо написания кода синхронизации вроде следующего:

public void AddOne()
{
   lock(myLockToken)
   {
      intVal++;
   }
}

можно воспользоваться статическим методом Interlocked.Increment() и в результате упростить код. Этому методу нужно передать по ссылке переменную для увеличения. Обратите внимание, что метод Increment() не только изменяет значение входного параметра, но также возвращает полученное новое значение:

public void AddOne()
{
   int newVal = Interlocked.Increment(ref intVal);
}

В дополнение к Increment и Decrement тип Interlocked позволяет автоматически присваивать числовые и объектные данные. Например, чтобы присвоить значение 83 переменной-члену, можно обойтись без явного оператора lock (или явной логики Monitor) и применить вместо этого метод Interlock.Exchange():

public void SafeAssignment()
{
   Interlocked.Exchange(ref mylnt, 83);
}

Класс Monitor

Компилятор C# преобразует оператор lock в код, использующий класс Monitor. Например, показанный ниже оператор lock:

lock (obj)
{
   // синхронизированная область для obj
}

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

Компилятор помещает вызов метода Exit() в обработчик finally блока try, чтобы блокировка снималась даже в случае генерации исключения:

Monitor.Enter(obj);
try
{
   // синхронизированная область для obj
}
finally
{
   Monitor.Exit(obj);
}

Класс Monitor обладает одним важным преимуществом по сравнению с оператором lock в C#: он позволяет добавлять значение тайм-аута для ожидания получения блокировки. Таким образом, вместо того, чтобы ожидать блокировку до бесконечности, можно вызвать метод TryEnter и передать в нем значение тайм-аута, указывающее, сколько максимум времени должно ожидаться получение блокировки.

Когда блокировка obj получена, метод TryEnter() устанавливает булевский параметр ref в true и производит синхронизированный доступ к состоянию, охраняемому объектом obj. Если obj блокируется другим потоком на протяжении более 500 миллисекунд, то TryEnter() устанавливает переменную lockTaken в false и поток больше не ожидает, а используется для выполнения другой работы. Возможно, позже поток попытается получить блокировку еще раз.

Атрибут [Synchronization]

Последний из примитивов синхронизации, которые здесь рассматриваются — это атрибут [Synchronization], который является членом пространства имен System.Runtime.Remoting.Contexts. Этот атрибут уровня класса эффективно блокирует весь код членов экземпляра объекта, обеспечивая безопасность в отношении потоков.

Когда среда CLR размещает объекты, снабженные атрибутами [Synchronization], она помещает объект в контекст синхронизации. Объекты, которые не должны выходить за границы контекста, должны наследоваться от ContextBoundObject. Поэтому, чтобы сделать класс MyThread безопасным к потокам (без явного написания кода внутри членов класса), необходимо модифицировать его следующим образом:

using System.Runtime.Remoting.Contexts;
...
// Все методы MyThread теперь безопасны к потокам!
[Synchronization]
public class MyThread : ContextBoundObject
{
   public void ThreadNumbers()
   {...}
}

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

Пройди тесты
Лучший чат для C# программистов