Выбрать главу
ет в другом потоке, этот поток установит событийный объект в сигнальное состояние, вызвав метод Set(). Поэтому метод Set() следует рассма тривать как уведомляющий о том, что событие произошло. После установки собы тийного объекта в сигнальное состояние произойдет немедленный возврат из метода WaitOne(), и первый поток возобновит свое выполнение. А в результате вызова мето да Reset() событийный объект возвращается в несигнальное состояние. Событие типа AutoResetEvent отличается от события типа ManualResetEvent лишь способом установки в исходное состояние. Если для события типа ManualResetEvent событийный объект остается в сигнальном состоянии до тех пор, пока не будет вызван метод Reset(), то для события типа AutoResetEvent собы тийный объект автоматически переходит в несигнальное состояние, как только поток, ожидающий это событие, получит уведомление о нем и возобновит свое выполне ние. Поэтому если применяется событие типа AutoResetEvent, то вызывать метод Reset() необязательно. В приведенном ниже примере программы демонстрируется применение события типа ManualResetEvent. // Использовать событийный объект, устанавливаемый // в исходное состояние вручную. using System; using System.Threading; // Этот поток уведомляет о том, что событие передано его конструктору. class MyThread { public Thread Thrd; ManualResetEvent mre; public MyThread(string name, ManualResetEvent evt) { Thrd = new Thread(this.Run); Thrd.Name = name; mre = evt; Thrd.Start(); } // Точка входа в поток. void Run() { Console.WriteLine("Внутри потока " + Thrd.Name); for(int i=0; i<5; i++) { Console.WriteLine(Thrd.Name); Thread.Sleep(500); } Console.WriteLine(Thrd.Name + " завершен!"); // Уведомить о событии. mre.Set(); } } class ManualEventDemo { static void Main() { ManualResetEvent evtObj = new ManualResetEvent(false); MyThread mt1 = new MyThread("Событийный Поток 1", evtObj); Console.WriteLine("Основной поток ожидает событие."); // Ожидать уведомления о событии. evtObj.WaitOne(); Console.WriteLine("Основной поток получил " + "уведомление о событии от первого потока."); // Установить событийный объект в исходное состояние. evtObj.Reset(); mt1 = new MyThread("Событийный Поток 2", evtObj); // Ожидать уведомления о событии. evtObj.WaitOne(); Console.WriteLine("Основной поток получил " + "уведомление о событии от второго потока."); } } Ниже приведен результат выполнения рассматриваемой здесь программы, хотя у вас он может оказаться несколько иным. В потоке Событийный Поток 1 Событийный Поток 1 Основной поток ожидает событие. Событийный Поток 1 Событийный Поток 1 Событийный Поток 1 Событийный Поток 1 Событийный Поток 1 завершен! Основной поток получил уведомление о событии от первого потока. В потоке Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 завершен! Основной поток получил уведомление о событии от второго потока. Прежде всего обратите внимание на то, что событие типа ManualResetEvent передается непосредственно конструктору класса MyThread. Когда завершается ме тод Run() из класса MyThread, он вызывает для событийного объекта метод Set(), устанавливающий этот объект в сигнальное состояние. В методе Main() формирует ся событийный объект evtObj типа ManualResetEvent, первоначально устанавли ваемый в исходное, несигнальное состояние. Затем создается экземпляр объекта типа MyThread, которому передается событийный объект evtObj. После этого основной поток ожидает уведомления о событии. А поскольку событийный объект evtObj пер воначально находится в несигнальном состоянии, то основной поток вынужден ожи дать до тех пор, пока для экземпляра объекта типа MyThread не будет вызван метод Set(), устанавливающий событийный объект evtObj в сигнальное состояние. Это дает возможность основному потоку возобновить свое выполнение. Затем событийный объект устанавливается в исходное состояние, и весь процесс повторяется, но на этот раз для второго потока. Если бы не событийный объект, то все потоки выполнялись бы одновременно, а результаты их выполнения оказались бы окончательно запутан ными. Для того чтобы убедиться в этом, попробуйте закомментировать вызов метода WaitOne() в методе Main(). Если бы в рассматриваемой здесь программе событийный объект типа AutoResetEvent использовался вместо событийного объекта типа ManualResetEvent, то вызывать метод Reset() в методе Main() не пришлось бы. Ведь в этом случае со бытийный объект автоматически устанавливается в несигнальное состояние, когда по ток, ожидающий данное событие, возобновляет свое выполнение. Для опробования этой разновидности события замените в данной программе все ссылки на объект типа ManualResetEvent ссылками на объект типа AutoResetEvent и удалите все вызовы ме тода Reset(). Видоизмененная версия программы будет работать так же, как и прежде. Класс Interlocked Еще одним классом, связанным с синхронизацией, является класс Interlocked. Этот класс служит в качестве альтернативы другим средствам синхронизации, когда требуется только изменить значение общей переменной. Методы, доступные в классе Interlocked, гарантируют, что их действие будет выполняться как единая, непреры ваемая операция. Это означает, что никакой синхронизации в данном случае вообще не требуется. В классе Interlocked предоставляются статические методы для сложе ния двух целых значений, инкрементирования и декрементирования целого значения, сравнения и установки значений объекта, обмена объектами и получения 64-разрядно- го значения. Все эти операции выполняются без прерывания. В приведенном ниже примере программы демонстрируется применение двух ме тодов из класса Interlocked:Increment() и Decrement(). При этом используются следующие формы обоих методов: public static int Increment(ref int location) public static int Decrement(ref int location) где location — это переменная, которая подлежит инкрементированию или декре ментированию. // Использовать блокируемые операции. using System; using System.Threading; // Общий ресурс. class SharedRes { public static int Count = 0; } // В этом потоке переменная SharedRes.Count инкрементируется. class IncThread { public Thread Thrd; public IncThread(string name) { Thrd = new Thread(this.Run); Thrd.Name = name; Thrd.Start(); } // Точка входа в поток. void Run() { for(int i=0; i<5; i++) { Interlocked.Increment(ref SharedRes.Count); Console.WriteLine(Thrd.Name + " Count = " + SharedRes.Count); } } } // В этом потоке переменная SharedRes.Count декрементируется. class DecThread { public Thread Thrd; public DecThread(string name) { Thrd = new Thread(this.Run); Thrd.Name = name; Thrd.Start(); } // Точка входа в поток. void Run() { for(int i=0; i<5; i++) { Interlocked.Decrement(ref SharedRes.Count); Console.WriteLine(Thrd.Name + " Count = " + SharedRes.Count); } } } class InterlockedDemo { static void Main() { // Сконструировать два потока. IncThread mt1 = new IncThread("Инкрементирующий Поток"); DecThread mt2 = new DecThread("Декрементирующий Поток"); mt1.Thrd.Join(); mt2.Thrd.Join(); } } Классы синхронизации, внедренные в версии .NET Framework 4.0 Рассматривавшиеся ранее классы синхронизации, в том числе Semaphore и AutoResetEvent, были доступны в среде .NET Framework, начиная с версии 1.1. Таким образом, эти классы образуют основу поддержки синхронизации в среде .NET Framework. Но после выпуска версии .NET Framework 4.0 появился ряд новых альтер натив этим классам синхронизации. Все они перечисляются ниже. Класс Назначение Barrier Вынуждает потоки ожидать появления всех остальных пото ков в указанной точке, называемой барьерной CountdownEvent Выдает сигнал, когда обратный отсчет завершается ManualResetEventSlim Это упрощенный вариант класса ManualResetEvent SemaphoreSlim Это упрощенный вариант класса Semaphore Если вам понятно, как пользоваться основными классами синхронизации, описан ными ранее в этой главе, то у вас не должно возникнуть затруднений при использова нии их новых альтернатив и дополнений. Прерывание потока Иногда поток полезно прервать до его нормального завершения. Например, от ладчику может понадобиться прервать вышедший из-под контроля поток. После пре рывания поток удаляется из системы и не может быть начат снова. Для прерывания потока до его нормального завершения служит метод Thread. Abort(). Ниже приведена простейшая форма этого метода. public void Abort() Метод Abort() создает необходимые условия Для генерирования исключения ThreadAbortException в том потоке, для которого он был вызван. Это исключение приводит к прерыванию потока и может быть перехвачено и в коде программы, но в этом случае оно автоматически генерируется еще раз, чтобы остановить поток. Ме тод Abort() не всегда способен остановить поток немедленно, поэтому если поток требуется остановить перед тем, как продолжить выполнение программы, то после метода Abort() следует сразу же вызвать метод Join(). Кроме того, в самых редких случаях методу Abort() вообще не удается остановить поток. Это происходит, напри мер, в том случае, если кодовый блок finally входит в бесконечный цикл. В приведенном ниже примере программы демонстрируется применение метода Abort() для прерывания потока. // Прервать поток с помощью метода Abort(). using System; using System.Threading; class MyThread { public Thread Thrd; public MyThread(string name) { Thrd = new Thread(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 StopDemo { static void Main() { MyThread mt1 = new MyThread("Мой Поток"); Thread.Sleep(1000); // разрешить порожденному потоку начать свое выполнение Console.WriteLine("Прерывание потока."); mt1.Thrd.Abort(); 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 Прерывание потока. Основной поток прерван. ПРИМЕЧАНИЕ Метод Abort() не следует применять в качестве обычного средства прерывания потока, поскольку он предназначен для особых случаев. Обычно поток должен завершаться естествен ным образом, чтобы произошел возврат из метода, выполняющего роль точки входа в него. Другая форма метода Abort() В некоторых случаях оказывается полезной другая форма метода Abort(), при веденная ниже в общем виде: public void Abort (object stateInfo) где stateInfo обозначает любую информацию, которую требуется передать по току, когда он останавливается. Эта информация доступна посредством свойства ExceptionState из класса исключения ThreadAbortException. Подобным образом потоку можно передать код завершения. В приведенном ниже примере программы демонстрируется применение данной формы метода Abort(). // Использовать форму метода Abort (object stateInfo). using System; using System.Threading; class MyThread { public Thread Thrd; public MyThread(string name) { Thrd = new Thread(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 + " завершен нормально."); } catch(ThreadAbortException exc) { Console.WriteLine("Поток прерван, код завершения " + exc.ExceptionState); } } } class UseAltAbort { 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 Прерывание потока. Поток прерван, код завершения 100 Основной поток прерван. Как следует из приведенного выше результата, значение 100 передается методу Abort() в качестве кода прерывания. Это значение становится затем доступным по средством свойства ExceptionState из класса исключения ThreadAbortException, которое перехватывается потоком при его прерывании. Отмена действия метода Abort() Запрос на преждевременное прерывание может быть переопределен в са мом потоке. Для этого необходимо сначала перехватить в потоке исключение ThreadAbortException, а затем вызвать метод ResetAbort(). Благодаря этому исключается повторное генерирование исключения по завершении обработчика ис ключения, прерывающего данный поток. Ниже приведена форма объявления метода ResetAbort(). public static void ResetAbort() Вызов метода ResetAbort() может завершиться неудачно, если в потоке отсутству ет надлежащий режим надежной отмены преждевременного прерывания потока. В приведенном ниже примере программы демонстрируется применение метода ResetAbort(). // Использовать метод ResetAbort(). using System; using System.Threading; class MyThread { public Thread Thrd; public MyThread(string name) { Thrd = new Thread(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 { 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("Основной поток прерван."); } } Ниже приведен результат выполнения этой программы. Мой Поток начат 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(), и выполнение этого потока продолжается. Любое другое значе ние аргумента приведет к прерыванию потока. Приостановка и возобновление потока В первоначальных версиях среды .NET Framework поток можно было приостано вить вызовом метода Thread.Suspend() и возобновить вызовом метода Thread. Resume(). Но теперь оба эти метода считаются устаревшими и не рекомендуются к применению в новом коде. Объясняется это, в частности, тем, что пользоваться мето дом Suspend() на самом деле небезопасно, так как с его помощью можно приостано вить поток, который в настоящий момент удерживает блокировку, что препятствует ее снятию, а следовательно, приводит к взаимоблокировке. Применение обоих мето дов может стать причиной серьезных осложнений на уровне системы. Поэтому для приостановки и возобновления потока следует использовать другие средства синхро низации, в том числе мьютекс и семафор. Определение состояния потока Состояние потока может быть получено из свойства ThreadState, доступного в классе Thread. Ниже приведена общая форма этого свойства. public ThreadState 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; } Данное свойство возвращает ссылку на тот поток, в котором оно используется. По этому если свойство CurrentThread используется при выполнении кода в основном потоке, то с его помощью можно получить ссылку на основной поток. Имея в своем распоряжении такую ссылку, можно управлять основным потоком так же, как и лю бым другим потоком. В приведенном ниже примере программы сначала получается ссылка на основной поток, а затем получаются и устанавливаются имя и приоритет основного потока. // Продемонстрировать управление основным потоком. using System; using System.Threading; class UseMain { 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 Следует, однако, быть очень внимательным, выполняя операции с основным пото ком. Так, если добавить в конце метода Main() следующий вызов метода Join(): Thrd.Join(); программа никогда не завершится, поскольку она будет ожидать окончания основного потока! Дополнительные средства многопоточной обработки, внедренные в версии .NET Framework 4.0 В версии .NET Framework 4.0 внедрен ряд новых средств многопоточной обработки, которые могут оказаться весьма полезными. Самым важным среди них является новая система отмены. В этой системе поддерживается механизм отмены потока простым, вполне определенным и структурированным способом. В основу этого механизма по ложено понятие признака отмены, с помощью которого указывается состояние отмены потока. Признаки отмены поддерживаются в классе CancellationTokenSource и в структуре CancellationToken. Система отмены полностью интегрирована в новую библиотеку распараллеливания задач (TPL), и поэтому она подробнее рассматривает ся вместе с TPL в главе 24. В класс System.Threading добавлена структура SpinWait, предоставляющая ме тоды SpinOnce() и SpinUntil(), которые обеспечивают более полный контроль над ожиданием в состоянии занятости. Вообще говоря, структура SpinWait оказывается непригодной для однопроцессорных систем. А для многопроцессорных систем она применяется в цикле. Еще одним элементом, связанным с ожиданием в состоянии за нятости, является структура SpinLock, которая применяется в цикле ожидания до тех пор, пока не станет доступной блокировка. В класс Thread добавлен метод Yield(), который просто выдает остаток кванта времени, выделенного потоку. Ниже приведена общая форма объявления этого метода. public static bool Yield() Этот метод возвращает логическое значение true, если происходит переключение контекста. В отсутствие другого потока, готового для выполнения, переключение кон текста не произойдет. Рекомендации по многопоточному программированию Для эффективного многопоточного программирования самое главное — мыслить категориями параллельного, а не последовательного выполнения кода. Так, если в одной программе имеются две подсистемы, которые могут работать параллельно, их следует организовать в отдельные потоки. Но делать это следует очень внимательно и аккуратно, поскольку если создать слишком много потоков, то тем самым можно значительно снизить, а не повысить производительность программы. Следует также иметь в виду дополнительные издержки, связанные с переключением контекста. Так, если создать слишком много потоков, то на смену контекста уйдет больше времени ЦП, чем на выполнение самой программы! И наконец, для написания нового кода, предназначенного для многопоточной обработки, рекомендуется пользоваться библи отекой распараллеливания задач (TPL), о которой речь пойдет в следующей главе. Запуск отдельной задачи Многозадачность на основе потоков чаще всего организуется при программирова нии на С#. Но там, где это уместно, можно организовать и многозадачность на основе процессов. В этом случае вместо запуска другого потока в одной и той же программе одна программа начинает выполнение другой. При программировании на C# это делается с помощь