Книга: C# 4.0: полное руководство
Сообщение между потоками с помощью методов Wait(), Pulse() и PulseAll()
Сообщение между потоками с помощью методов Wait(), Pulse() и PulseAll()
Рассмотрим следующую ситуацию. Поток T выполняется в кодовом блоке lock
, и ему требуется доступ к ресурсу R, который временно недоступен. Что же тогда делать потоку T? Если поток T войдет в организованный в той или иной форме цикл опроса, ожидая освобождения ресурса R, то тем самым он свяжет соответствующий объект, блокируя доступ к нему других потоков. Это далеко не самое оптимальное решение, поскольку оно лишает отчасти преимуществ программирования для многопоточной среды. Более совершенное решение заключается в том, чтобы временно освободить объект и тем самым дать возможность выполняться другим потокам. Такой подход основывается на некоторой форме сообщения между потоками, благодаря которому один поток может уведомлять другой о том, что он заблокирован и что другой поток может возобновить свое выполнение. Сообщение между потоками организуется в C# с помощью методов Wait(), Pulse()
и PulseAll()
.
Методы Wait(), Pulse()
и PulseAll()
определены в классе Monitor
и могут вызываться только из заблокированного фрагмента блока. Они применяются следующим образом. Когда выполнение потока временно заблокировано, он вызывает метод Wait()
. В итоге поток переходит в состояние ожидания, а блокировка с соответствующего объекта снимается, что дает возможность использовать этот объект в другом потоке. В дальнейшем ожидающий поток активизируется, когда другой поток войдет в аналогичное состояние блокировки, и вызывает метод Pulse()
или PulseAll()
. При вызове метода Pulse()
возобновляется выполнение первого потока, ожидающего своей очереди на получение блокировки. А вызов метода PulseAll()
сигнализирует о снятии блокировки всем ожидающим потокам.
Ниже приведены две наиболее часто используемые формы метода Wait()
.
public static bool Wait(object obj)
public static bool Wait(object obj, int миллисекунд_простоя)
В первой форме ожидание длится вплоть до уведомления об освобождении объекта, а во второй форме — как до уведомления об освобождении объекта, так и до истечения периода времени, на который указывает количество миллисекунд_простоя. В обеих формах obj обозначает объект, освобождение которого ожидается.
Ниже приведены общие формы методов Pulse()
и PulseAll()
:
public static void Pulse(object obj)
public static void PulseAll(object obj)
где obj обозначает освобождаемый объект.
Если методы Wait(),Pulse()
и PulseAll()
вызываются из кода, находящегося за пределами синхронизированного кода, например из блока lock
, то генерируется исключение SynchronizationLockException
.
Пример использования методов Wait() и Pulse()
Для того чтобы стало понятнее назначение методов Wait()
и Pulse()
, рассмотрим пример программы, имитирующей тиканье часов и отображающей этот процесс на экране словами "тик" и "так". Для этой цели в программе создается класс TickTock
, содержащий два следующих метода: Tick()
и Тоск()
. Метод Tick()
выводит на экран слово "тик", а метод Тоск()
— слово "так". Для запуска часов далее в программе создаются два потока: один из них вызывает метод Tick()
, а другой — метод Тоск()
. Преследуемая в данном случае цель состоит в том, чтобы оба потока выполнялись, поочередно выводя на экран слова "тик" и "так", из которых образуется повторяющийся ряд "тик-так", имитирующий ход часов.
//Использовать методы Wait() и Pulse() для иммитации
//тиканья часов
using System;
using System.Threading;
class TickTock {
object lockOn = new object();
public void Tick(bool running) {
lock(lockOn) {
if(!running) { // остановить часы
Monitor.Pulse(lockOn); // уведомить любые ожидающие потоки
return;
}
Console.Write("тик ");
Monitor.Pulse(lockOn); // разрешить выполнение метода Tock()
Monitor.Wait(lockOn); // ожидать завершения метода Tock()
}
}
public void Tock(bool running) {
lock(lockOn) {
if(!running) { // остановить часы
Monitor.Pulse(lockOn); // уведомить любые ожидающие потоки
return;
}
Console.WriteLine("так");
Monitor.Pulse(lockOn); // разрешить выполнение метода Tick()
Monitor.Wait(lockOn); // ожидать завершения метода Tick()
}
}
}
class MyThread {
public Thread Thrd;
TickTock ttOb;
// Сконструировать новый поток.
public MyThread(string name, TickTock tt) {
Thrd = new Thread(this.Run);
ttOb = tt;
Thrd.Name = name;
Thrd.Start();
}
// Начать выполнение нового потока,
void Run() {
if(Thrd.Name == "Tick") {
for(int i=0; i<5; i++)
ttOb.Tick(true);
ttOb.Tick(false) ;
}
else {
for(int i=0; i<5; i++)
ttOb.Tock(true);
ttOb.Tock(false);
}
}
}
class TickingClock {
static void Main() {
TickTock tt = new TickTock();
MyThread mt1 = new MyThread("Tick", tt);
MyThread mt2 = new MyThread("Tock", tt);
mt1.Thrd.Join();
mt2.Thrd.Join();
Console.WriteLine("Часы остановлены");
}
}
Ниже приведен результат выполнения этой программы.
тик так
тик так
тик так
тик так
тик так
Часы остановлены
Рассмотрим эту программу более подробно. В методе Main()
создается объект tt
типа TickTock
, который используется для запуска двух потоков на выполнение. Если в методе Run()
из класса MyThread
обнаруживается имя потока Tick
, соответствующее ходу часов "тик", то вызывается метод Tick()
. А если это имя потока Tock
, соответствующее ходу часов "так", то вызывается метод Tock()
. Каждый из этих методов вызывается пять раз подряд с передачей логического значения true
в качестве аргумента. Часы идут до тех пор, пока этим методам передается логическое значение true
, и останавливаются, как только передается логическое значение false
.
Самая важная часть рассматриваемой здесь программы находится в методах Tick()
и Tock()
. Начнем с метода Tick()
, код которого для удобства приводится ниже.
public void Tick(bool running) {
lock(lockOn) {
if(!running) { // остановить часы
Monitor.Pulse(lockOn); // уведомить любые ожидающие потоки
return;
}
Console.Write("тик ");
Monitor.Pulse(lockOn); // разрешить выполнение метода Tock()
Monitor.Wait(lockOn); // ожидать завершения метода Tock()
}
}
Прежде всего обратите внимание на код метода Tick()
в блоке lock
. Напомним, что методы Wait()
и Pulse()
могут использоваться только в синхронизированных блоках кода. В начале метода Tick()
проверяется значение текущего параметра, которое служит явным признаком остановки часов. Если это логическое значение false
, то часы остановлены. В этом случае вызывается метод Pulse(), разрешающий выполнение любого потока, ожидающего своей очереди. Мы еще вернемся к этому моменту в дальнейшем. Если же часы идут при выполнении метода Tick()
, то на экран выводится слово "тик" с пробелом, затем вызывается метод Pulse()
, а после него — метод Wait()
. При вызове метода Pulse()
разрешается выполнение потока для того же самого объекта, а при вызове метода Wait()
выполнение метода Tick()
приостанавливается до тех пор, пока метод Pulse()
не будет вызван из другого потока. Таким образом, когда вызывается метод Tick()
, отображается одно слово "тик" с пробелом, разрешается выполнение другого потока, а затем выполнение данного метода приостанавливается.
Метод Тоск()
является точной копией метода Tick()
, за исключением того, что он выводит на экран слово "так". Таким образом, при входе в метод Тоск()
на экран выводится слово "так", вызывается метод Pulse()
, а затем выполнение метода Тоск()
приостанавливается. Методы Tick()
и Тоск()
можно рассматривать как поочередно сменяющие друг друга, т.е. они взаимно синхронизированы.
Когда часы остановлены, метод Pulse()
вызывается для того, чтобы обеспечить успешный вызов метода Wait()
. Напомним, что метод Wait()
вызывается в обоих методах, Tick()
и Тоск()
, после вывода соответствующего слова на экран. Но дело в том, что когда часы остановлены, один из этих методов все еще находится в состоянии ожидания. Поэтому завершающий вызов метода Pulse()
требуется, чтобы выполнить ожидающий метод до конца. В качестве эксперимента попробуйте удалить этот вызов метода Pulse()
и понаблюдайте за тем, что при этом произойдет. Вы сразу же обнаружите, что программа "зависает", и для выхода из нее придется нажать комбинацию клавиш <Ctrl+C>. Дело в том, что когда метод Wait()
вызывается в последнем вызове метода Тоск()
, соответствующий ему метод Pulse()
не вызывается, а значит, выполнение метода Тоск()
оказывается незавершенным, и он ожидает своей очереди до бесконечности.
Прежде чем переходить к чтению следующего раздела, убедитесь сами, если, конечно, сомневаетесь, в том, что следует обязательно вызывать методы Wait()
и Pulse()
, чтобы имитируемые часы шли правильно. Для этого подставьте приведенный ниже вариант класса TickTock
в рассматриваемую здесь программу. В этом варианте все вызовы методов Wait()
и Pulse()
исключены.
// Нерабочий вариант класса TickTock.
class TickTock {
object lockOn = new object();
public void Tick(bool running) {
lock(lockOn) {
if (!running) { // остановить часы
return;
}
Console.Write("тик ") ;
}
}
public void Tock (bool running) {
lock(lockOn) {
if(!running) { // остановить часы
return;
}
Console.Write("так ") ;
}
}
}
После этой подстановки результат выполнения данной программы будет выглядеть следующим образом.
тик так так так так так тик тик тик тик Часы остановлены
Очевидно, что методы Tick()
и Tock()
больше не синхронизированы!
- Основы многопоточной обработки
- Класс Thread
- Определение момента окончания потока
- Передача аргумента потоку
- Свойство IsBackground
- Приоритеты потоков
- Синхронизация
- Сообщение между потоками с помощью методов Wait(), Pulse() и PulseAll()
- Взаимоблокировка и состояние гонки
- Применение атрибута MethodlmplAttribute
- Применение мьютекса и семафора
- Применение событий
- Класс Interlocked
- Классы синхронизации, внедренные в версии .NET Framework 4.0
- Прерывание потока
- Приостановка и возобновление потока
- Определение состояния потока
- Применение основного потока
- Дополнительные средства многопоточной обработки, внедренные в версии .NET Framework 4.0
- Рекомендации по многопоточному программированию
- Запуск отдельной задачи
- Классы синхронизации, внедренные в версии .NET Framework 4.0
- Миграция между различными версиями InterBase
- 3.4. Отношения между классами
- Повышение производительности приложений с помощью хранимых процедур
- Тестирование Web-сервиса XML с помощью WebDev.WebServer.exe
- Организация пользователей в группы с помощью ролей
- Мост между физической и логической структурой базы данных
- Что делать, если при установке принтера появляется сообщение Невозможно завершение операции. Подсистема печати недоступн...
- При копировании с жесткого диска на «флэшку» иногда появляется сообщение о дополнительной присоединенной информации, кот...
- Сообщение канала
- Множественные интерфейсы и имена методов
- Метасообщение