Методы Wait(), Pulse() и PulseAll

57

Рассмотрим следующую ситуацию. Поток 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(), рассмотрим пример программы, имитирующей тиканье часов и отображающей этот процесс на экране словами "тик" и "так". Для этой цели в программе создается класс TickTock, содержащий два следующих метода: Tick() и Tock(). Метод Tick() выводит на экран слово "тик", а метод Tock() — слово "так".

Для запуска часов далее в программе создаются два потока: один из них вызывает метод Tick(), а другой — метод Tock(). Преследуемая в данном случае цель состоит в том, чтобы оба потока выполнялись, поочередно выводя на экран слова "тик" и "так", из которых образуется повторяющийся ряд "тик-так", имитирующий ход часов:

using System;
using System.Threading;

namespace _Pulse___Wait
{
    class TickTock
    {
        private object lockOn = new object();

        public void Tick(bool running)
        {
            lock (lockOn)
            {
                if (!running)
                {
                    // Остановить часы
                    Monitor.Pulse(lockOn);
                    return;
                }

                Console.Write("Тик ");
                // Разрешить выполнение метода Tock()
                Monitor.Pulse(lockOn);

                // Ожидать завершение Tock()
                Monitor.Wait(lockOn);
            }
        }

        public void Tock(bool running)
        {
            lock (lockOn)
            {
                if (!running)
                {
                    Monitor.Pulse(lockOn);
                    return;
                }

                Console.WriteLine("так");
                Monitor.Pulse(lockOn);
                Monitor.Wait(lockOn);
            }
        }
    }

    class MyThread
    {
        public Thread thrd;
        TickTock ttobj;

        // Новый поток
        public MyThread(string name, TickTock tt)
        {
            thrd = new Thread(this.Run);
            ttobj = tt;
            thrd.Name = name;
            thrd.Start();
        }

        void Run()
        {
            if (thrd.Name == "Tick")
            {
                for (int i = 0; i < 5; i++)
                    ttobj.Tick(true);
                ttobj.Tick(false);
            }
            else
            {
                for (int i = 0; i < 5; i++)
                    ttobj.Tock(true);
                ttobj.Tock(false);
            }
        }
    }

    class Program
    {
        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("Часы остановлены");
            Console.ReadLine();
        }
    }
}
Применение методов Wait() и Pulse()

Рассмотрим эту программу более подробно. В методе Main() создается объект tt типа TickTock, который используется для запуска двух потоков на выполнение. Если в методе Run() из класса MyThread обнаруживается имя потока Tick, соответствующее ходу часов "тик", то вызывается метод Tick(). А если это имя потока Tock, соответствующее ходу часов "так", то вызывается метод Tock().

Каждый из этих методов вызывается пять раз подряд с передачей логического значения true в качестве аргумента. Часы идут до тех пор, пока этим методам передается логическое значение true, и останавливаются, как только передается логическое значение false. Самая важная часть рассматриваемой здесь программы находится в методах Tick() и Tock().

Прежде всего обратите внимание на код метода Tick() в блоке lock. Напомним, что методы Wait() и Pulse() могут использоваться только в синхронизированных блоках кода. В начале метода Tick() проверяется значение текущего параметра, которое служит явным признаком остановки часов. Если это логическое значение false, то часы остановлены. В этом случае вызывается метод Pulse(), разрешающий выполнение любого потока, ожидающего своей очереди.

Если же часы идут при выполнении метода Tick(), то на экран выводится слово "тик" с пробелом, затем вызывается метод Pulse(), а после него — метод Wait(). При вызове метода Pulse() разрешается выполнение потока для того же самого объекта, а при вызове метода Wait() выполнение метода Tick() приостанавливается до тех пор, пока метод Pulse() не будет вызван из другого потока. Таким образом, когда вызывается метод Tick(), отображается одно слово "тик" с пробелом, разрешается выполнение другого потока, а затем выполнение данного метода приостанавливается.

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