Замечание. Если пытаться блокировать программный код в статическом методе, вы, очевидно, не можете использовать ключевое слово this. Но в этом случае можно передать объект System.Type соответствующего класса с помощью оператора C# typeof.
Если снова выполнить это приложение, вы увидите, что теперь каждый поток получает возможность закончить свою работу (рис. 14.10).
Рис. 14.10. Конкуренция в действии, третья попытка
Исходный код. Проект MultiThreadedPrinting размещен в подкаталоге, соответствующем главе 14.
Синхронизация с помощью типа System.Threading.Monitor
Оператор C# lock на самом деле является лишь ключевым словом, обозначающим использование типа класса System.Threading.Monitor. После обработки компилятором C# контекст блокировки превращается в следующее (вы можете убедиться в этом с помощью ildasm.exe).
public void PrintNumbers() {
Monitor.Enter(this);
try {
// Вызов информации Thread.
Console.WriteLine("-› {0} выполняет PrintNumbers()", Thread.CurrentThread.Name); // Вывод чисел.
Console.Write("Ваши числа: ");
for (int i = 0; i ‹ 10; i++) {
Random r = new Random();
Thread.Sleep(1000* r.Next(5));
Console.Write(i + ", ");
}
Console.WriteLine();
} finallу {
Monitor.Exit(this);
}
}
Во-первых, заметим, что конечным получателем маркера потока, который был указан в качестве аргумента ключевого слова lock, является метод Monitor.Enter(). Во-вторых, весь программный код в рамках контекста соответствующей блокировки помещен в блок try. Соответствующий блок finally гарантирует, что маркер потока будет освобожден (с помощью метода Monitor.Exit()), независимо от исключений, которые могут возникать в среде выполнения. Если изменить программу MultiThreadSharedData так, чтобы тип Monitor использовался непосредственно (как это будет сделано чуть позже), то ее вывод останется тем же.
При использовании ключевого слова lock, кажется, требуется меньший ввод программного кода, чем при явном использований типа System.Threading.Monitor, поэтому вы можете задать вопрос о преимуществах непосредственного использования типа Monitor. Краткий ответ: контроль. При использовании типа Monitor вы можете дать указание активному потоку подождать (с помощью метода Wait()), информировать ожидающие потоки о завершении текущего потока (с помощью методов Pulse() и PulseAll()) и т.д.
В большинстве случаев вам будет вполне достаточно возможностей, обеспечиваемых ключевым словам C# lock. Но если вы захотите рассмотреть другие члены класса Monitor, обратитесь к документации .NET Framework 2.0 SDK.
Синхронизация с помощью типа System.Threading.Interlocked
В это всегда верится с трудом, пока вы не проверите соответствующий программный код CIL, но и операции присваивания, и базовые арифметические операции не являются атомарными. Поэтому в пространстве имен System.Threading предлагается тип, позволяющий воздействовать на отдельный элемент данных атомарно с меньшей нагрузкой, чем это делает тип Monitor. Тип класса Interlocked определяет статические члены, описания которых приведены в табл. 14.4.
Таблица 14.4. Члены типа System.Threading.Interlocked
Член Описание CompareExchange() Безопасно проверяет два значения на равенство, и если они равны, заменяет одно из значений третьим Decrement() Безопасно уменьшает значение на 1 Exchange() Безопасно меняет два значения местами Increment() Безопасно выполняет приращение значения на 1
Хотя это может и не казаться очевидным на первый взгляд, процесс атомарного изменения одного значения является вполне типичным в многопоточном окружении. Предположим, что у нас есть метод AddOne(), который увеличивает целочисленную переменную intVal на единицу. Вместо программного кода синхронизации, подобного следующему;
public void AddOne() {
lock(this) {
intVal++;
}
}
можно предложить более простой программный код, в котором используется статический метод Interlocked.Increment(). Просто передайте переменную для приращения по ссылке. Обратите внимание на то, что метод Increment() не только изменяет значение поступающего параметра, но и возвращает новое значение.
public void AddOne() {
int newVal = Interlocked.Increment(ref intVal);
}
В дополнение к Increment() и Decrement() тип Interlocked позволяет атомарно присваивать числовые и объектные данные. Например, если вы хотите присвоить члену-переменной значение 83, вы можете избежать необходимости явного использования оператора lock (или явного применения логики Monitor), если используете метод Interlocked.Exchange().
public void SafeAssignment() {
Interlocked.Exchange(ref myInt, 83);
}
Наконец, при проверке двух значений на равенство, чтобы обеспечить потоковую безопасность элементу сравнения, можете использовать метод Interlocked.CompareExchange(), как показано ниже.
public void CompareAndExchange() {
// Если значением i является 83, изменить его на 99.
Interlocked.CompareExchange(ref i, 99, 83);
}
Синхронизация с помощью атрибута [Synchronization]
Последним из рассмотренных здесь примитивов синхронизации будет атрибут [Synchronization], который определяется в пространстве имен System.Runtime.Remoting.Contexts. Этот атрибут уровня класса для безопасности потока эффективно блокирует весь программный код членов экземпляра. Когда среда CLR размещает объект, имеющий атрибут [Synchronization], она помещает этот объект в рамки синхронизированного контекста. Вы должны помнить из главы 13, что объекты, которые не должны покидать контекстные границы, являются производными от ContextBoundObject. Поэтому, чтобы сделать тип класса Printer устойчивым в отношении потоков (без добавления программного кода защиты потоков вручную), следует изменить соответствующее определение так.
using System.Runtime.Remoting.Contexts;
...
// Все методы Printer теперь потокоустойчивы!
[Synchronization]
public class Printer: СontextBoundObject {
public void PrintNumbers() {
…
}
}
Этот подход можно назвать способом создания потокоустойчивого программного кода для ленивых, поскольку здесь не требуется выяснять, какие фрагменты типа могут испытывать влияние внешних потоков. Главным недостатком этого подхода является то, что даже если какой-то метод и не испытывает влияния внешних потоков, среда CLR все равно блокирует обращение к этому методу. Очевидно, это может ухудшить общие характеристики функционирования типа, так что используйте указанный подход с осторожностью.
Итак, мы с вами обсудили целый ряд подходов к решению вопроса синхронизированного доступа к общим блокам данных. Будьте уверены, в пространстве имен System.Threading есть и другие типы, которые я настоятельно рекомендую вам постепенно исследовать. В завершение этой главы, посвященной программированию потоков, давайте рассмотрим три: дополнительных типа: TimerCallback, Timer и ThreadPool.
Программирование с помощью таймеров обратного вызова
Во многих приложениях возникает необходимость вызывать конкретный метод через регулярные промежутки времени. Например, в одном приложении может потребоваться отображение текущего времени в строке состояния с помощью некоторой вспомогательной функции. В другом приложении может понадобиться периодический вызов вспомогательной функции, выполняющей в фоновом режиме какие-то некритические задачи, например проверку поступления новых сообщений электронной почты. Для таких ситуаций можно использовать тип System. Threading.Timer в совокупности с соответствующим делегатом TimerCallback.
Для примера предположим, что нам нужно создать консольное приложение, которое ежесекундно выводит текущее время, пока пользователь не нажмет клавишу, завершающую выполнение этого приложения. Первым очевидным шагом здесь является создание метода, который будет вызываться типом Timer.
class TimePrinter {
static void PrintTime(object state) {
Console.WriteLine("Время: {0}", DateTime.Now.ToLongTimeString());
}
}
Этот метод имеет один параметр типа System.Object и возвращает void. Такая структура метода обязательна, поскольку делегат TimerCallback может вызывать только такие методы. Значение, передаваемое целевому методу делегата TimerCallback, может представлять любую информацию (так, в случае электронной почты это может быть имя сервера Microsoft Exchange, с которым требуется взаимодействие в ходе процесса). А так как параметр является типом System.Object, в действительности можно передать любое число аргументов, если использовать System.Array или пользовательский класс (структуру).