Классы Interlocked и Monitor
68C# и .NET --- Многопоточность и файлы --- Классы Interlocked и Monitor
Класс 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 будет по-прежнему блокировать вызовы этого метода. Очевидно, что это приведет к деградации общей функциональности типа, поэтому используйте такую технику с осторожностью.