Книга: 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() больше не синхронизированы!

Оглавление книги


Генерация: 6.227. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз