Рейтинговые книги
Читем онлайн C# 4.0: полное руководство - Герберт Шилдт

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 234 235 236 237 238 239 240 241 242 ... 294

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

Взаимоблокировка и состояние гонки

При разработке многопоточных программ следует быть особенно внимательным, чтобы избежать взаимоблокировки и состояний гонок. Взаимоблокировка, как подразумевает само название, — это ситуация, в которой один поток ожидает определенных действий от другого потока, а другой поток, в свою очередь, ожидает чего-то от первого потока. В итоге оба потока приостанавливаются, ожидая друг друга, и ни один из них не выполняется. Эта ситуация напоминает двух слишком вежливых людей, каждый из которых настаивает на том, чтобы другой прошел в дверь первым!

На первый взгляд избежать взаимоблокировки нетрудно, но на самом деле не все так просто, ведь взаимоблокировка может возникать окольными путями. В качестве примера рассмотрим класс TickTock из предыдущей программы. Как пояснялось выше, в отсутствие завершающего вызова метода Pulse() из метода Tick() или Tock() тот или другой будет ожидать до бесконечности, что приведет к "зависанию" программы вследствие взаимоблокировки. Зачастую причину взаимоблокировки не так-то просто выяснить, анализируя исходный код программы, поскольку параллельно действующие процессы могут взаимодействовать довольно сложным образом во время выполнения. Для исключения взаимоблокировки требуется внимательное программирование и тщательное тестирование. В целом, если многопоточная программа периодически "зависает", то наиболее вероятной причиной этого является взаимоблокировка.

1 ... 234 235 236 237 238 239 240 241 242 ... 294
На этой странице вы можете бесплатно читать книгу C# 4.0: полное руководство - Герберт Шилдт бесплатно.
Похожие на C# 4.0: полное руководство - Герберт Шилдт книги

Оставить комментарий