Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
592 Часть II. Библиотека C# 1. Если для заданного объекта инструкция lock размещена в некотором блоке кода, этот объект блокируется, и никакой другой поток не сможет запросить блокировку. 2. Другие потоки, пытающиеся запросить блокировку для того же объекта, перейдут в состояние ожидания и будут находиться в нем до тех пор, пока код не будет разблокирован. 3. Объект разблокируется, когда поток выходит из заблокированного блока. Необходимо также отметить, что инструкция lock должна использоваться только для объектов, которые определены либо как private- , либо как internal- объекты. В противном случае внешние по отношению к вашей программе потоки смогут получать блокировку и не снимать ее. Альтернативное решение Несмотря на то что блокирование кода метода, как показано в предыдущей программе, не представляет большой сложности и является эффективным средством синхронизации, оно, к сожалению, работает не во всех случаях. Например, нужно получить синхронизированный доступ к методу класса, который создавали не вы лично, а в нем самом средств синхронизации изначально не предусмотрено. Такая ситуация возможна в том случае, если вы хотите использовать класс, который написан сторонней организацией, и невозможно получить доступ к его исходному коду. В этом случае, очевидно, вам не удастся добавить инструкцию lock в соответствующий метод класса. Как тогда синхронизировать доступ к объекту такого класса? К счастью, есть очень простое решение описанной проблемы: заблокировать доступ к объекту из внешнего (по отношению к объекту) кода, указав этот объект в инструкции lock . Например, рассмотрим альтернативную реализацию предыдущей программы. Обратите внимание на то, что код в методе sumIt() больше не блокируется. Теперь блокируются обращения к методу sumIt() внутри класса MyThread // Еще один способ использовать инструкцию lock // для синхронизации доступа к объекту. using System; using System.Threading; class SumArray { int sum; public int sumIt(int[] nums) { sum =0; // Установка начального значения суммы. for(int i=0; i < nums.Length; i++) { sum += nums[i]; Console.WriteLine( "Промежуточная сумма " + Thread.CurrentThread.Name + " равна " + sum); Thread.Sleep(10); // Разрешение переключения задач. } return sum; } } class MyThread { Глава 21. Многопоточное программирование 593 public Thread thrd; int[] a; int answer; /* Создаем один объект класса SumArray для всех экземпляров класса MyThread. */ static SumArray sa = new SumArray(); // Создаем новый поток. public MyThread(string name, int[] nums) { a = nums; thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; thrd.Start(); // Запускаем поток на выполнение. } // Начало выполнения нового потока. void run() { Console.WriteLine(thrd.Name + " стартовал."); // Инструкция lock содержит вызов метода sumIt(). lock(sa) answer = sa.sumIt(a); Console.WriteLine("Сумма для потока " + thrd.Name + " равна " + answer); Console.WriteLine(thrd.Name + " завершен."); } } class Sync { public static void Main() { int[] a = {1, 2, 3, 4, 5}; MyThread mt1 = new MyThread("Потомок #1", a); MyThread mt2 = new MyThread("Потомок #2", a); mt1.thrd.Join(); mt2.thrd.Join(); } } Здесь блокируется обращение к методу sa.sumIt() , а не код в самом методе sumIt() . Это реализуется таким способом: // Инструкция lock содержит вызов метода sumIt(). lock(sa) answer = sa.sumIt(a); При выполнении этой программы получаем такие же корректные результаты, как и при использовании исходного решения. Блокирование статического метода Поскольку блокировка работает по отношению к объекту, то на первый взгляд может показаться, что невозможно заблокировать код static -метода, поскольку не существует объекта, для которого требуется выполнить блокировку. В действительности все обстоит иначе. Чтобы заблокировать static -метод, достаточно использовать инструкцию lock в следующем формате: 594 Часть II. Библиотека C# lock(typeof( class )) { // Блокируемый код. } Здесь class представляет собой имя класса, в котором содержится static -метод, подлежащий блокировке. Класс Monitor и инструкция lock Ключевое слово lock — это не что иное, как сокращенный вариант использования средств синхронизации, определенных в классе Monitor , принадлежащем пространству имен System.Threading . В классе Monitor определено несколько методов синхронизации. Например, чтобы получить возможность блокировки для некоторого объекта, вызовите метод Enter() , а чтобы снять блокировку — метод Exit() . Эти методы имеют следующий формат public static void Enter(object syncOb ) public static void Exit(object syncOb ) Здесь syncOb — синхронизируемый объект. Если при вызове метода Enter() заданный объект недоступен, вызывающий поток будет ожидать до тех пор, пока объект не станет доступным. Разработчики из компании Microsoft утверждают, что lock -блок “совершенно эквивалентен” вызову метода Enter() с последующим вызовом метода Exit() . Но поскольку lock — это встроенная инструкция языка C#, то для получения блокировки в C#-программировании предпочтительнее использовать именно ее. Обратите внимание на метод TryEnter() из класса Monitor . Один из форматов его использования таков: public static bool TryEnter(object syncOb ) Метод возвращает значение true, если вызывающий поток получает блокировку для объекта syncOb , и значение false в противном случае. Если заданный объект недоступен, вызывающий поток будет ожидать до тех пор, пока он не станет доступным. В классе Monitor определены еще три метода: Wait() , Pulse() и PulseAll() Они описаны в следующем разделе. Взаимодействие потоков с помощью методов Wait(), Pulse() и PulseAll() Рассмотрим следующую ситуацию. Поток (назовем его T ) выполняет содержимое lock -блока и требует доступ к ресурсу (назовем его R ), который временно недоступен. Что делать потоку T ? Если поток T войдет в цикл опроса в ожидании доступности ресурса R , он свяжет объект, блокируя доступ к нему другим потокам. Это решение трудно назвать оптимальным, поскольку оно аннулирует преимущества программирования в многопоточной среде. Будет лучше, если поток T временно откажется от “претензий” на объект, позволив другому потоку выполнить свою работу. Когда же ресурс R станет доступным, поток T можно уведомить об этом, и он возобновит выполнение. Такой подход опирается на межпоточные средства общения, которые позволяют одному потоку уведомить другой о том, что он блокируется, а затем первого поставить в известность о том, что он может возобновить выполнение. C# поддерживает межпоточное взаимодействие с помощью методов Wait() , Pulse() и PulseAll() Методы Wait() , Pulse() и PulseAll() определены в классе Monitor . Эти методы можно вызывать только внутри lock -блока кода. Когда выполнение потока Глава 21. Многопоточное программирование 595 временно блокируется, вызывается метод Wait() , т.е. он переходит в режим ожидания (“засыпает”) и снимает блокировку с объекта, позволяя другому потоку использовать этот объект. Позже, когда другой поток входит в аналогичное состояние блокирования и вызывает метод Pulse() или PulseAll() , “спящий” поток “просыпается”. Обращение к методу Pulse() возобновляет выполнение потока, стоящего первым в очереди потоков, пребывающих в режиме ожидания. Обращение к методу PulseAll() сообщает о снятии блокировки всем ожидающим потокам. Вот два наиболее употребимых формата метода Wait() : public static bool Wait(object waitOb ) public static bool Wait(object waitOb , int milliseconds ) Первый формат означает ожидание до уведомления. Второй — подразумевает ожидание до уведомления или до истечения периода времени, заданного в миллисекундах. В обоих случаях параметр waitOb задает объект, к которому ожидается доступ. Форматы использования методов Pulse() и PulseAll() таковы: public static void Pulse(object waitOb ) public static void PulseAll(object waitOb ) Здесь параметр waitOb означает объект, освобождаемый от блокировки. Если метод Wait() , Pulse() или PulseAll() вызывается из кода, который находится вне lock - блока, генерируется исключение типа SynchronizationLockException Пример использования методов Wait() и Pulse() Чтобы понять необходимость применения методов Wait() и Pulse() , создадим программу, которая имитирует тиканье часов посредством отображения на экране слов “тик” и “так”. Для этого создадим класс TickTock , который содержит два метода: tick() и tock() . Метод tick() отображает слово “тик”, а метод tock() — слово “так”. Для работы нашего “часового механизма” создаем два потока, причем один из них будет вызывать метод tick() , а другой — метод tock() . Наша задача — организовать выполнение этих потоков таким образом, чтобы программа последовательно отображала “тик-так”, т.е. сначала слово “тик”, а за ним — слово “так”. // Использование методов Wait() и Pulse() для создания // тикающих часов. using System; using System.Threading; class TickTock { public void tick(bool running) { lock(this) { if(!running) { // Останов часов. Monitor.Pulse(this); // Уведомление любых // ожидающих потоков. return; } Console.Write("тик"); Monitor.Pulse(this); // Разрешает выполнение // метода tock(). Monitor.Wait(this); // Ожидаем завершения // метода tock(). } 596 Часть II. Библиотека C# } public void tock(bool running) { lock(this) { if(!running) { // Останов часов. Monitor.Pulse(this); // Уведомление любых // ожидающих потоков. return; } Console.WriteLine("так"); Monitor.Pulse(this); // Разрешает выполнение // метода tick(). Monitor.Wait(this); // Ожидаем завершения // метода tick(). } } } class MyThread { public Thread thrd; TickTock ttOb; // Создаем новый поток. public MyThread(string name, TickTock tt) { thrd = new Thread(new ThreadStart(this.run)); ttOb = tt; thrd.Name = name; thrd.Start(); } // Начинаем выполнение нового потока. void run() { if(thrd.Name == "тик") { 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 { public static void Main() { TickTock tt = new TickTock(); MyThread mt1 = new MyThread("тик", tt); MyThread mt2 = new MyThread("так", tt); mt1.thrd.Join(); mt2.thrd.Join(); Console.WriteLine("Часы остановлены"); } } При выполнении эта программа сгенерировала следующие результаты: Глава 21. Многопоточное программирование 597 тик так тик так тик так тик так тик так Часы остановлены Рассмотрим эту программу в деталях. В методе Main() создается объект класса TickTock с именем tt , который затем используется для запуска двух потоков. В методе run() класса MyThread выполняется сравнение имени текущего потока со словом “тик”. Если сравниваемые значения совпали, вызывается метод tick() , в противном случае — метод tock() . При этом для каждого из пяти вызовов каждого метода в качестве аргумента передается значение true . Часы “тикают” до тех пор, пока передается именно значение true . Последний вызов каждого метода (с передачей в качестве аргумента значения false ) останавливает часы. Самая важная часть программы сосредоточена в методах tick() и tock() Рассмотрим сначала метод tick() , который для удобства приведем ниже: public void tick(bool running) { lock(this) { if(!running) { // Останов часов. Monitor.Pulse(this); // Уведомление любых // ожидающих потоков. return; } Console.Write("тик "); Monitor.Pulse(this); // Разрешает выполнение // метода tock(). Monitor.Wait(this); // Ожидаем завершения // метода tock(). } } Прежде всего обратите внимание на то, что код метода tick() заключен в рамки lock -блока. Вспомните, методы Wait() и Pulse() можно использовать только внутри синхронизируемых блоков. Метод начинается с проверки значения параметра running Этот параметр используется для отключения часов. Если он равен значению false , часы будут остановлены. В этом случае вызывается метод Pulse() , который позволяет заработать ожидающему потоку. Сюда мы скоро вернемся, а пока предположим, что часы никто (пока) не останавливает, и на экране отображается слово “тик”, а затем происходит обращение к методу Pulse() с последующим вызовом метода Wait() . Обращение к методу Pulse() позволяет перейти в ожидание возможности доступа к тому же самому объекту. Вызов метода Wait() вынуждает метод tick() приостановиться до тех пор, пока другой поток не вызовет метод Pulse() . Таким образом, после вызова метода tick() отображается одно слово “тик”, выдается разрешение на “оживление” другого потока, после чего первый приостанавливается. Метод tock() — точная копия метода tick() , за исключением того, что он отображает слово “так”. Итак, после его вызова отображается одно слово “так”, вызывается метод Pulse() , а затем — и метод Wait() . Если рассматривать эти методы в паре, то за вызовом метода tick() может следовать только вызов метода tock() , за которым, в свою очередь, может следовать только вызов метода tick() и т.д.. Таким образом, эти два метода взаимно синхронизированы. Для вызова метода Pulse() при останове часов есть все основания, поскольку он позволяет успешно завершить последнее обращение к методу Wait() . Не забывайте, 598 Часть II. Библиотека C# что методы tick() и tock() после отображения соответствующего сообщения вызывают метод Wait() . Следовательно, в момент останова часов один из этих методов находится в состоянии ожидания. А чтобы вывести его из этого состояния, необходимо вызвать метод Pulse() в последний раз. Ради эксперимента попробуйте удалить этот вызов метода Pulse() и посмотрите, как это скажется на результатах выполнения программы. Нетрудно убедиться, что программа в этом случае “зависнет” и для выхода из нее вам придется нажать клавиши Wait() из последнего выполнения метода tock() нет соответствующего обращения к методу Pulse() , который бы позволил бы методу tock() благополучно завершиться. Поэтому он (метод tock() ) находится в бесконечном ожидании. Если у вас есть какие-либо сомнения насчет необходимости вызовов методов Wait() и Pulse() для правильной работы наших “часов”, то прежде чем переходить к следующему разделу, замените класс TickTock в предыдущей программе приведенной ниже версией. Здесь из класса удалены обращения к методам Wait() и Pulse() // Нерабочая версия класса TickTock. class TickTock { public void tick(bool running) { lock(this) { if(!running) { // Останов часов. return; } Console.Write("тик "); } } public void tock(bool running) { lock(this) { if(!running) { // Останов часов. return; } Console.WriteLine("так"); } } } После замены класса TickTock результаты выполнения программы “часов” выглядят так: тик тик тик тик тик так так так так так Часы остановлены В этом случае очевидно, что методы tick() и tock() больше не синхронизированы! Взаимоблокировка При разработке многопоточных программ необходимо позаботиться о том, чтобы во время их выполнения не создалась тупиковая ситуация, вызванная взаимоблокировкой. При взаимоблокировке (deadlock) один поток ожидает, пока другой не выполнит Глава 21. Многопоточное программирование 599 некоторое действие, но в то же время второй поток ожидает действия первого. Таким образом, оба потока приостановлены, ожидая друг друга, и ни один из них не выполняется. Эта ситуация напоминает двух чрезмерно вежливых людей, каждый из которых настаивает на том, чтобы другой прошел в дверь первым! Казалось бы, избежать взаимоблокировки нетрудно, но это не совсем так. Например, взаимоблокировка может возникнуть в ответвлениях программы. Рассмотрим класс TickTock . Как разъяснялось выше, если метод Pulse() не выполнится в последний раз из метода tick() или tock() , то другой метод (т.е. tock() или tick() ) будет находиться в состоянии бесконечного ожидания, и программа зависнет. Зачастую причину взаимоблокировки нелегко понять, просто изучая исходный код программы, поскольку одновременно выполняющиеся потоки могут сложным образом взаимодействовать во время работы. Чтобы избежать взаимоблокировки, необходимо очень аккуратно подходить к программированию и тщательно тестировать написанный код. Как правило, если многопоточная программа вдруг “виснет”, то наиболее вероятная причина этого — взаимоблокировка. Использование атрибута MethodImplAttribute Используя атрибут MethodImplAttribute , можно синхронизировать метод целиком. Этот подход можно рассматривать как альтернативу инструкции lock в случаях, когда необходимо заблокировать все содержимое метода. Атрибут MethodImplAttribute определен в пространстве имен System.Runtime.CompilerServices Конструктор, используемый для синхронизации, имеет такой вид: public MethodImplAttribute(MethodImplOptions opt ) Здесь параметр opt задает атрибут реализации. Для синхронизации метода задайте атрибут MethodImplOptions.Synchronized . Этот атрибут обеспечивает блокировку всего метода. Рассмотрим версию класса TickTock , в которой для синхронизации используется атрибут MethodImplAttribute : // Использование атрибута MethodImplAttribute // для синхронизации метода. using System; using System.Threading; using System.Runtime.CompilerServices; class TickTock { /* Следующий атрибут синхронизирует метод tick() целиком. */ [MethodImplAttribute(MethodImplOptions.Synchronized)] public void tick(bool running) { if(!running) { // Останов часов. Monitor.Pulse(this); // Уведомление для // ожидающих потоков. return; } Console.Write("Тик "); Monitor.Pulse(this); // Разрешаем работать 600 Часть II. Библиотека C# // методу tock(). Monitor.Wait(this); // Ожидаем, пока не завершится // метод tock(). } /* Следующий атрибут синхронизирует метод tock() целиком. */ [MethodImplAttribute(MethodImplOptions.Synchronized)] public void tock(bool running) { if(!running) { // Останов часов. Monitor.Pulse(this); // Уведомление для // ожидающих потоков. return; } Console.WriteLine("Так"); Monitor.Pulse(this); // Разрешаем работать // методу tick(). Monitor.Wait(this); // Ожидаем, пока не завершится // метод tick(). } } class MyThread { public Thread thrd; TickTock ttOb; // Создаем новый поток. public MyThread(string name, TickTock tt) { thrd = new Thread(new ThreadStart(this.run)); ttOb = tt; thrd.Name = name; thrd.Start(); } // Начинаем выполнять новый поток. void run() { if(thrd.Name == "Тик") { 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 { public static void Main() { TickTock tt = new TickTock(); MyThread mt1 = new MyThread("Тик", tt); MyThread mt2 = new MyThread("Так", tt); Глава 21. Многопоточное программирование 601 mt1.thrd.Join(); mt2.thrd.Join(); Console.WriteLine("Часы остановлены."); } } Результаты выполнения программы с этим вариантом класса TickTock совпадают с приведенным выше (имеются в виду правильно работающие “часы”). При блокировании всего метода выбор между lock -инструкцией или атрибутом MethodImplAttribute за вами. Оба средства дают одинаковые результаты. Но поскольку lock — ключевое слово языка C#, то в примерах этой книги предпочтение отдано именно встроенному средству синхронизации. Приостановка, возобновление и завершение выполнения потоков Иногда выполнение потока необходимо приостановить. Например, поток можно использовать для отображения времени суток. Если пользователь желает убрать часы с экрана, соответствующий поток можно приостановить. Позже, когда необходимость в часах появится снова, выполнение приостановленного потока можно возобновить. В любом случае приостановить и возобновить поток — дело нехитрое. Иногда нужно и вовсе завершить выполнение потока. Завершение выполнения потока отличается от приостановки тем, что завершенный поток удаляется из системы, и его выполнение не может быть возобновлено в дальнейшем. Для приостановки потока используйте метод Thread.Suspend() , а для его возобновления — метод Thread.Resume() . Форматы использования этих методов таковы: public void Suspend() public void Resume() Если вызывающий поток находится не в подходящем для вызываемого метода состоянии, генерируется исключение типа ThreadStateException . Такие последствия может иметь, например, попытка возобновить поток, который не был приостановлен. Чтобы завершить поток, используйте метод Thread.Abort() . Самый простой формат его использования выглядит так: public void Abort() Метод Abort() генерирует исключение типа ThreadAbortException для потока, из которого этот метод вызван. Это исключение и заставляет поток завершиться. Кроме того, то же самое исключение может быть перехвачено программным кодом (с автоматической его регенерацией для завершения потока). Однако следует учитывать, что метод Abort() не всегда способен немедленно остановить выполнение потока, поэтому, если важно, чтобы поток был завершен до продолжения вашей программы, необходимо сопроводить вызов метода Abort() вызовом метода Join() . В некоторых (довольно редких) случаях метод Abort() не в состоянии завершить поток. Это возможно в ситуации, когда finally -блок включен в бесконечный цикл. В следующем примере демонстрируется приостановка, возобновление и завершение выполнения потока: //Приостановка, возобновление и завершение потока. using System; using System.Threading; 602 Часть II. Библиотека C# class MyThread { public Thread thrd; public MyThread(string name) { thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; thrd.Start(); } // Это входная точка для потока. void run() { Console.WriteLine(thrd.Name + " стартовал."); for(int i = 1; i <= 1000; i++) { Console.Write(i + " "); if((i%10)==0) { Console.WriteLine(); Thread.Sleep(250); } } Console.WriteLine(thrd.Name + " завершен."); } } class SuspendResumeStop { public static void Main() { MyThread mt1 = new MyThread("Мой поток"); Thread.Sleep(1000); // Разрешаем стартовать // дочернему потоку. mt1.thrd.Suspend(); Console.WriteLine("Приостановка выполнения потока."); Thread.Sleep(1000); mt1.thrd.Resume(); Console.WriteLine("Возобновление выполнения потока."); Thread.Sleep(1000); mt1.thrd.Suspend(); Console.WriteLine("Приостановка выполнения потока."); Thread.Sleep(1000); mt1.thrd.Resume(); Console.WriteLine("Возобновление выполнения потока."); Thread.Sleep(1000); Console.WriteLine("Завершение выполнения потока."); mt1.thrd.Abort(); mt1.thrd.Join(); // Ожидаем завершения выполнения потока. Console.WriteLine("Основной поток завершен."); } } Глава 21. Многопоточное программирование 603 Результаты выполнения этой программы таковы: Мой поток стартовал. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 Приостановка выполнения потока. 41 42 43 44 45 46 47 48 49 50 Возобновление выполнения потока. 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 Приостановка выполнения потока. 81 82 83 84 85 86 87 88 89 90 Возобновление выполнения потока. 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 Завершение выполнения потока. Основной поток завершен. Альтернативный формат использования метода Abort() В некоторых случаях удобней использовать второй формат метода Abort() : public void Abort(object info ) Здесь параметр info содержит информацию, которую необходимо передать в поток при его завершении. Эта информация доступна через свойство ExceptionState класса ThreadAbortException . Этот формат можно использовать для передачи потоку кода завершения, что и демонстрируется в следующем примере: // Использование метода Abort(object). using System; using System.Threading; class MyThread { public Thread thrd; public MyThread(string name) { thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; thrd.Start(); } // Это входная точка для потока. void run() { try { Console.WriteLine(thrd.Name + " стартовал."); for(int i = 1; i <= 1000; i++) { Console.Write(i + " "); if((i%10)==0) { Console.WriteLine(); Thread.Sleep(250); } } Console.WriteLine(thrd.Name + " завершился нормально."); 604 Часть II. Библиотека C# } catch(ThreadAbortException exc) { Console.WriteLine( "Выполнение потока прервано, код завершения = " + exc.ExceptionState); } } } class UseAltAbort { public static void Main() { MyThread mt1 = new MyThread("Мой поток"); Thread.Sleep(1000); // Разрешаем стартовать // дочернему потоку. Console.WriteLine("Прерывание выполнения потока."); mt1.thrd.Abort(100); mt1.thrd.Join(); // Ожидаем завершения // выполнения потока. Console.WriteLine("Основной поток завершен."); } } Результаты выполнения этой таковы: Мой поток стартовал. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 Прерывание выполнения потока. 41 42 43 44 45 46 47 48 49 50 Выполнение потока прервано, код завершения = 100 Основной поток завершен. Как подтверждают результаты выполнения этой программы, методу Abort() передано число 100. Это значение затем можно прочитать в свойстве ExceptionState класса исключения ThreadAbortException , перехватываемого потоком при завершении. Отмена действия метода Abort() Поток может переопределить запрос на прерывание выполнения. Для этого поток должен перехватить исключение типа ThreadAbortException , а затем вызвать метод ResetAbort() . Это защищает исключение от автоматической регенерации по окончании его обработки потоком. Метод ResetAbort() объявляется следующим образом: public static void ResetAbort() Обращение к методу ResetAbort() может оказаться неудачным, если поток не имеет соответствующего уровня доступа, чтобы отменить прерывание выполнения. Использование метода ResetAbort() демонстрируется в следующей программе: // Использование метода ResetAbort(). using System; using System.Threading; class MyThread { public Thread thrd; Глава 21. Многопоточное программирование 605 public MyThread(string name) { thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; thrd.Start(); } // Это входная точка для потока. void run() { Console.WriteLine(thrd.Name + " стартовал."); for(int i = 1; i <= 1000; i++) { try { Console.Write(i + " "); if((i%10)==0) { Console.WriteLine(); Thread.Sleep(250); } } catch(ThreadAbortException exc) { if((int)exc.ExceptionState ==0) { Console.WriteLine( "Прерывание отменено! Код завершения = " + exc.ExceptionState); Thread.ResetAbort(); } else Console.WriteLine( "Выполнение потока прервано, код завершения = " + exc.ExceptionState); } } Console.WriteLine(thrd.Name + " завершился нормально."); } } class ResetAbort { public static void Main() { MyThread mt1 = new MyThread("Мой поток"); Thread.Sleep(1000); // Разрешаем стартовать // дочернему потоку. Console.WriteLine("Прерывание выполнения потока."); mt1.thrd.Abort(0); // Это не остановит выполнение // потока. Thread.Sleep(1000); // Разрешаем дочернему потоку // поработать немного дольше. Console.WriteLine("Прерывание выполнения потока."); mt1.thrd.Abort(100); // Эта инструкция в состоянии // остановить выполнение потока. mt1.thrd.Join(); // Ожидаем завершения // выполнения потока. Console.WriteLine("Основной поток завершен."); } } 606 Часть II. Библиотека C# Результаты выполнения этой программы таковы: Мой поток стартовал. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 Прерывание выполнения потока. Прерывание отменено! Код завершения = 0 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 Прерывание выполнения потока. Выполнение потока прервано, код завершения = 100 Основной поток завершен. Вы убедились, что, если метод Abort() вызывается с аргументом, значение которого равно нулю, то посредством вызова метода ResetAbort() запрос на прерывание отменяется, и поток продолжает выполняться. Любое другое значение этого аргумента останавливает выполнение потока. Определение состояния потока Состояние потока можно получить из свойства ThreadState , определенного в классе Thread : public Thread ThreadState{ get; } Состояние потока возвращается как значение, определенное перечислением ThreadState . В нем определены такие значения: ThreadState.Aborted ThreadState.AbortRequested ThreadState.Background ThreadState.Running ThreadState.Stopped ThreadState.StopRequested ThreadState.Suspended ThreadState.SuspendRequested ThreadState.Unstarted ThreadState.WaitSleepJoin Одно из перечисленных выше значений требует пояснения. В состояние, представленное значением ThreadState WaitSleepJoin , поток входит, ожидая результатов вызова метода Wait() , Sleep() или Join() Использование основного потока Как упоминалось в начале этой главы, все C#-программы имеют по крайней мере один поток управления, именуемый основным потоком, который автоматически создается в начале выполнения программы. Основной поток обрабатывается подобно всем остальным потокам. Чтобы получить доступ к основному потоку, необходимо иметь объект класса Thread , который его представляет. Это реализуется с помощью свойства CurrentThread , которое является членом класса Thread . Формат его использования таков: public static Thread CurrentThread{ get; } Это свойство возвращает ссылку на поток, в котором оно опрашивается. Следовательно, если при выполнении основного потока обратиться к свойству Глава 21. Многопоточное программирование 607 CurrentThread , мы получим ссылку на основной поток. Имея такую ссылку, можно управлять основным потоком, как и любым другим. В следующей программе демонстрируется, как получить ссылку на основной поток, узнать его имя и приоритет, а также задать новое имя и приоритет: // Управление основным потоком. using System; using System.Threading; class UseMain { public static void Main() { Thread thrd; // Получаем ссылку на объект основного потока. thrd = Thread.CurrentThread; // Отображаем имя основного потока. if(thrd.Name == null) Console.WriteLine("Основной поток не имеет имени."); else Console.WriteLine("Имя основного потока: " + thrd.Name); // Отображаем приоритет основного потока. Console.WriteLine("Приоритет: " + thrd.Priority); Console.WriteLine(); // Задаем имя и приоритет. Console.WriteLine("Установка имени и приоритета.\n"); thrd.Name = "Основной поток"; thrd.Priority = ThreadPriority.AboveNormal; Console.WriteLine( "У основного потока теперь есть имя: " + thrd.Name); Console.WriteLine("Приоритет теперь таков: " + thrd.Priority); } } Результаты выполнения этой программы таковы: Основной поток не имеет имени. Приоритет: Normal Установка имени и приоритета. У основного потока теперь есть имя: Основной поток Приоритет теперь таков: AboveNormal Предупреждение: будьте осторожны при выполнении операций над основным потоком. Например, если обращение к методу Join() thrd.Join(); добавить в конец метода Main() , программа никогда не завершится, поскольку она будет ожидать, пока не завершится основной поток! 608 Часть II. Библиотека C# Совет по созданию многопоточных программ Ключ к эффективному использованию многопоточности лежит в “параллельном” мышлении (в противоположность последовательному). Например, если в вашей программе предполагается функционирование двух параллельно работающих подсистем, организуйте их в виде отдельных потоков. Но если создать слишком много потоков, реальное быстродействие программы может пострадать. Помните: каждое переключение контекста требует определенных расходов системных ресурсов. При большом количестве потоков на изменение контекста будет потрачено больше процессорного времени, чем на выполнение самой программы! Запуск отдельной задачи Несмотря на то что чаще всего в C# -программировании используется поточно- ориентированная многозадачность, в соответствующих случаях возможно применение и процессно-ориентированной многозадачности. В многозадачной среде, ориентированной на процессы, вместо запуска в той же программе еще одного потока, одна программа запускает на выполнение другую программу. В C# это реализуется с использованием класса Process, который определен в пространстве имен System.Diagnostics Самый простой способ запустить другую программу — использовать метод Start() , определенный в классе Process . Вот один из простейших его форматов: public static Process Start(string name ) Здесь параметр name означает имя выполняемого файла или файла, связанного с выполняемым. При завершении созданного процесса вызовите метод Close() , чтобы освободить память, занимаемую этим процессом. Метод Close() объявляется так: public void Close() Завершить процесс можно двумя способами. Если процесс представляет собой GUI- приложение, ориентированное на выполнение под управлением Windows, то для завершения такого процесса достаточно вызвать метод CloseMainWindow() , определяемый следующим образом: public bool CloseMainWindow() Этот метод посылает процессу сообщение, предписывающее ему остановиться. Метод возвращает значение true , если посланное сообщение получено. Метод возвращает значение false , если данное приложение не является GUI-программой или не имеет главного окна. Более того, метод CloseMainWindow() — лишь запрос на прекращение работы. Если он игнорируется приложением, завершение не состоится. Для безусловного завершения процесса необходимо вызвать метод Kill() : public void Kill() Однако использовать метод Kill() нужно с большой осторожностью. Он обеспечивает бесконтрольное завершение процесса. Любые несохраненные данные, связанные с этим процессом, будут, скорее всего, утеряны. Ожидать завершения процесса можно с помощью метода WaitForExit() . Его два возможных формата таковы: public void WaitForExit() public bool WaitForExit(int milliseconds ) Глава 21. Многопоточное программирование 609 Первый формат позволяет ожидать до тех пор, пока процесс не завершится, а второй — лишь в течение заданного числа миллисекунд. Метод WaitForExit() , используемый во втором формате, возвращает значение true , если в течение заданного промежутка времени процесс завершился, и значение false , если он все еще выполняется. Следующая программа демонстрирует создание процесса и ожидание его завершения. Программа запускает стандартную утилиту Windows WordPad.exe // Запуск нового процесса. using System; using System.Diagnostics; class StartProcess { public static void Main() { Process newProc = Process.Start("wordpad.exe"); Console.WriteLine("Новый процесс стартовал."); newProc.WaitForExit(); newProc.Close(); // Освобождаем ресурсы системы. Console.WriteLine("Новый процесс завершился."); } } При выполнении этой программы запускается текстовый редактор WordPad , и на экране появится сообщение “Новый процесс стартовал”. Затем программа будет ожидать, пока вы не закроете утилиту WordPad . После завершения работы текстового редактора WordPad отобразится последнее сообщение “Новый процесс завершился”. |