Выбрать главу
10); Console.WriteLine("Основной поток завершен."); } } Эта версия программы дает такой же результат, как и предыдущая. Обратите вни мание на то, что объект потока сохраняется в переменной Thrd из класса MyThread. Создание нескольких потоков В предыдущих примерах программ был создан лишь один порожденный поток. Но в программе можно породить столько потоков, сколько потребуется. Например, в следующей программе создаются три порожденных потока. // Создать несколько потоков исполнения. using System; using System.Threading; class MyThread { public int Count; public Thread Thrd; public MyThread(string name) { Count = 0; Thrd = new Thread(this.Run); Thrd.Name = name; Thrd.Start(); } // Точка входа в поток. void Run() { Console.WriteLine(Thrd.Name + " начат."); do { Thread.Sleep(500); Console.WriteLine("В потоке " + Thrd.Name + ", Count = " + Count); Count++; } while(Count < 10); Console.WriteLine(Thrd.Name + " завершен."); } } class MoreThreads { static void Main() { Console.WriteLine("Основной поток начат."); // Сконструировать три потока. MyThread mt1 = new MyThread("Потомок #1"); MyThread mt2 = new MyThread("Потомок #2") ; MyThread mt3 = new MyThread("Потомок #3"); do { Console.Write("."); Thread.Sleep(100); } while(mt1.Count < 10 || mt2.Count < 10 || mt3.Count < 10); Console.WriteLine("Основной поток завершен."); } } Ниже приведен один из возможных результатов выполнения этой программы Основной поток начат. .Потомок #1 начат. Потомок #2 начат. Потомок #3 начат. ....В потоке Потомок #1, Count = 0 В потоке Потомок #2, Count = 0 В потоке Потомок #3, Count = 0 .....В потоке Потомок #1, Count = 1 В потоке Потомок #2, Count = 1 В потоке Потомок #3, Count = 1 .....В потоке Потомок #1, Count = 2 В потоке Потомок #2, Count = 2 В потоке Потомок #3, Count = 2 .....В потоке Потомок #1, Count = 3 В потоке Потомок #2, Count = 3 В потоке Потомок #3, Count = 3 .....В потоке Потомок #1, Count = 4 В потоке Потомок #2, Count = 4 В потоке Потомок #3, Count = 4 .....В потоке Потомок #1, Count = 5 В потоке Потомок #2, Count = 5 В потоке Потомок #3, Count = 5 .....В потоке Потомок #1, Count = 6 В потоке Потомок #2, Count = 6 В потоке Потомок #3, Count = 6 .....В потоке Потомок #1, Count = 7 В потоке Потомок #2, Count = 7 В потоке Потомок #3, Count = 7 .....В потоке Потомок #1, Count = 8 В потоке Потомок #2, Count = 8 В потоке Потомок #3, Count = 8 .....В потоке Потомок #1, Count = 9 Поток #1 завершен. В потоке Потомок #2, Count = 9 Поток #2 завершен. В потоке Потомок #3, Count = 9 Поток #3 завершен. Основной поток завершен. Как видите, после того как все три потока начнут выполняться, они будут совместно использовать ЦП. Приведенный выше результат может отличаться в зависимости от среды выполнения, операционной системы и других внешних факторов, влияющих на выполнение программы. Определение момента окончания потока Нередко оказывается полезно знать, когда именно завершается поток. В предыду щих примерах программ для этой цели отслеживалось значение переменной Count. Но ведь это далеко не лучшее и не совсем пригодное для обобщения решение. Правда, в классе Thread имеются два других средства для определения момента окончания потока. С этой целью можно, прежде всего, опросить доступное только для чтения свойство IsAlive, определяемое следующим образом. public bool IsAlive { get; } Свойство IsAlive возвращает логическое значение true, если поток, для которо го оно вызывается, по-прежнему выполняется. Для "опробования" свойства IsAlive подставьте приведенный ниже фрагмент кода вместо кода в классе MoreThread из предыдущей версии многопоточной программы, как показано ниже. // Использовать свойство IsAlive для отслеживания момента окончания потоков. class MoreThreads { static void Main() { Console.WriteLine("Основной поток начат."); // Сконструировать три потока. MyThread mt1 = new MyThread("Поток #1"); MyThread mt2 = new MyThread("Поток #2"); MyThread mt3 = new MyThread("Поток #3"); do { Console.Write("."); Thread.Sleep(100); } while(mt1.Thrd.IsAlive && mt2.Thrd.IsAlive && mt3.Thrd.IsAlive); Console.WriteLine("Основной поток завершен."); } } При выполнении этой версии программы результат получается таким же, как и прежде. Единственное отличие заключается в том, что в ней используется свойство IsAlive для отслеживания момента окончания порожденных потоков. Еще один способ отслеживания момента окончания состоит в вызове метода Join(). Ниже приведена его простейшая форма. public void Join() Метод Join() ожидает до тех пор, пока поток, для которого он был вызван, не завершится. Его имя отражает принцип ожидания до тех пор, пока вызывающий по ток не присоединится к вызванному методу. Если же данный поток не был начат, то генерируется исключение ThreadStateException. В других формах метода Join() можно указать максимальный период времени, в течение которого следует ожидать завершения указанного потока. В приведенном ниже примере программы метод Join() используется для того, чтобы основной поток завершился последним. // Использовать метод Join(). using System; using System.Threading; class MyThread { public int Count; public Thread Thrd; public MyThread(string name) { Count = 0; Thrd = new Thread(this.Run); Thrd.Name = name; Thrd.Start(); } // Точка входа в поток. void Run() { Console.WriteLine(Thrd.Name + " начат."); do { Thread.Sleep(500); Console.WriteLine("В потоке " + Thrd.Name + ", Count = " + Count); Count++; } While(Count < 10); Console.WriteLine(Thrd.Name + " завершен."); } } // Использовать метод Join() для ожидания до тех пор, // пока потоки не завершатся. class JoinThreads { static void Main() { Console.WriteLine("Основной поток начат."); // Сконструировать три потока. MyThread mt1 = new MyThread("Потомок #1"); MyThread mt2 = new MyThread("Потомок #2"); MyThread mt3 = new MyThread("Потомок #3"); mt1.Thrd.Join(); Console.WriteLine("Потомок #1 присоединен."); mt2.Thrd.Join(); Console.WriteLine("Потомок #2 присоединен."); mt3.Thrd.Join(); Console.WriteLine("Потомок #3 присоединен."); Console.WriteLine("Основной поток завершен."); } } Ниже приведен один из возможных результатов выполнения этой программы. На помним, что он может отличаться в зависимости от среды выполнения, операционной системы и прочих факторов, влияющих на выполнение программы. Основной поток начат. Потомок #1 начат. Потомок #2 начат. Потомок #3 начат. В потоке Потомок #1, Count = 0 В потоке Потомок #2, Count = 0 В потоке Потомок #3, Count = 0 В потоке Потомок #1, Count = 1 В потоке Потомок #2, Count = 1 В потоке Потомок #3, Count = 1 В потоке Потомок #1, Count = 2 В потоке Потомок #2, Count = 2 В потоке Потомок #3, Count = 2 В потоке Потомок #1, Count = 3 В потоке Потомок #2, Count = 3 В потоке Потомок #3, Count = 3 В потоке Потомок #1, Count = 4 В потоке Потомок #2, Count = 4 В потоке Потомок #3, Count = 4 В потоке Потомок #1, Count = 5 В потоке Потомок #2, Count = 5 В потоке Потомок #3, Count = 5 В потоке Потомок #1, Count = 6 В потоке Потомок #2, Count = 6 В потоке Потомок #3, Count = 6 В потоке Потомок #1, Count = 7 В потоке Потомок #2, Count = 7 В потоке Потомок #3, Count = 7 В потоке Потомок #1, Count = 8 В потоке Потомок #2, Count = 8 В потоке Потомок #3, Count = 8 В потоке Потомок #1, Count = 9 Потомок #1 завершен. В потоке Потомок #2, Count = 9 Потомок #2 завершен. В потоке Потомок #3, Count = 9 Потомок #3 завершен. Потомок #1 присоединен. Потомок #2 присоединен. Потомок #3 присоединен. Основной поток завершен. Как видите, выполнение потоков завершилось после возврата из последовательного ряда вызовов метода Join(). Передача аргумента потоку Первоначально в среде .NET Framework нельзя было передавать аргумент потоку, когда он начинался, поскольку у метода, служившего в качестве точки входа в поток, не могло быть параметров. Если же потоку требовалось передать какую-то информацию, то к этой цели приходилось идти различными обходными путями, например исполь зовать общую переменную. Но этот недостаток был впоследствии устранен, и теперь аргумент может быть передан потоку. Для этого придется воспользоваться другими формами метода Start(), конструктора класса Thread, а также метода, служащего в качестве точки входа в поток. Аргумент передается потоку в следующей форме метода Start(). public void Start(object параметр) Объект, указываемый в качестве аргумента параметр, автоматически передается методу, выполняющему роль точки входа в поток. Следовательно, для того чтобы пе редать аргумент потоку, достаточно передать его методу Start(). Для применения параметризированной формы метода Start() потребуется сле дующая форма конструктора класса Thread: public Thread(ParameterizedThreadStart запуск) где запуск обозначает метод, вызываемый с целью начать выполнение пото ка. Обратите внимание на то, что в этой форме конструктора запуск имеет тип ParameterizedThreadStart, а не ThreadStart, как в форме, использовавшейся в предыдущих примерах. В данном случае ParameterizedThreadStart является де легатом, объявляемым следующим образом. public delegate void ParameterizedThreadStart(object obj) Как видите, этот делегат принимает аргумент типа object. Поэтому для правиль ного применения данной формы конструктора класса Thread у метода, служащего в качестве точки входа в поток, должен быть параметр типа object. В приведенном ниже примере программы демонстрируется передача аргумента потоку. // Пример передачи аргумента методу потока. using System; using System.Threading; class MyThread { public int Count; public Thread Thrd; // Обратите внимание на то, что конструктору класса // MyThread передается также значение типа int. public MyThread(string name, int num) { Count = 0; // Вызвать конструктор типа ParameterizedThreadStart // явным образом только ради наглядности примера. Thrd = new Thread(this.Run); Thrd.Name = name; // Здесь переменная num передается методу Start() // в качестве аргумента. Thrd.Start(num); } // Обратите внимание на то, что в этой форме метода Run() // указывается параметр типа object. void Run(object num) { Console.WriteLine(Thrd.Name + " начат со счета " + num); do { Thread.Sleep(500); Console.WriteLine("В потоке " + Thrd.Name + ", Count = " + Count); Count++; } while(Count < (int) num); Console.WriteLine(Thrd.Name + " завершен."); } } class PassArgDemo { static void Main() { // Обратите внимание на то, что число повторений // передается этим двум объектам типа MyThread. MyThread mt = new MyThread("Потомок #1", 5); MyThread mt2 = new MyThread("Потомок #2", 3); do { Thread.Sleep(100); } while (mt.Thrd.IsAlive'| mt2.Thrd.IsAlive); Console.WriteLine("Основной поток завершен."); } } Ниже приведен результат выполнения данной программы, хотя у вас он может ока заться несколько иным. Потомок #1 начат со счета 5 Потомок #2 начат со счета 3 В потоке Потомок #2, Count = 0 В потоке Потомок #1, Count = 0 В потоке Потомок #1, Count = 1 В потоке Потомок #2, Count = 1 В потоке Потомок #2, Count = 2 Потомок #2 завершен. В потоке Потомок #1, Count = 2 В потоке Потомок #1, Count = 3 В потоке Потомок #1, Count = 4 Потомок #1 завершен. Основной поток завершен. Как следует из приведенного выше результата, первый поток повторяется пять раз, а второй — три раза. Число повторений указывается в конструкторе класса MyThread и затем передается методу Run(), служащему в качестве точки входа в поток, с помо щью параметризированной формы ParameterizedThreadStart метода Start(). Свойство IsBackground Как упоминалось выше, в среде .NET Framework определены две разновидности потоков: приоритетный и фоновый. Единственное отличие между ними заключается в том, что процесс не завершится до тех пор, пока не окончится приоритетный поток, тогда как фоновые потоки завершаются автоматически по окончании всех приоритет ных потоков. По умолчанию создаваемый поток становится приоритетным. Но его можно сделать фоновым, используя свойство IsBackground, определенное в классе Thread, следующим образом. public bool IsBackground { get; set; } Для того чтобы сделать поток фоновым, достаточно присвоить логическое значение true свойству IsBackground. А логическое значение false указывает на то, что поток является приоритетным. Приоритеты потоков У каждого потока имеется свой приоритет, который отчасти определяет, насколько часто поток получает доступ к ЦП. Вообще говоря, низкоприоритетные потоки полу чают доступ к ЦП реже, чем высокоприоритетные. Таким образом, в течение заданно го промежутка времени низкоприоритетному потоку будет доступно меньше времени ЦП, чем высокоприоритетному. Как и следовало ожидать, время ЦП, получаемое по током, оказывает определяющее влияние на характер его выполнения и взаимодей ствия с другими потоками, исполняемыми в настоящий момент в системе. Следует иметь в виду, что, помимо приоритета, на частоту доступа потока к ЦП оказывают влияние и другие факторы. Так, если высокоприоритетный поток ожида ет доступа к некоторому ресурсу, например для ввода с клавиатуры, он блокируется, а вместо него выполняется низкоприоритетный поток. В подобной ситуации низко приоритетный поток может получать доступ к ЦП чаще, чем высокоприоритетный поток в течение определенного периода времени. И наконец, конкретное планирова ние задач на уровне операционной системы также оказывает влияние на время ЦП, выделяемое для потока. Когда порожденный поток начинает выполняться, он получает приоритет, уста навливаемый по умолчанию. Приоритет потока можно изменить с помощью свойства Priority, являющегося членом класса Thread. Ниже приведена общая форма дан ного свойства: public ThreadPriority Priority{ get; set; } где ThreadPriority обозначает перечисление, в котором определяются приведенные ниже значения приоритетов. ThreadPriority.Highest ThreadPriority.AboveNormal ThreadPriority.Normal ThreadPriority.BelowNormal ThreadPriority.Lowest По умолчанию для потока устанавливается значение приоритета ThreadPriority. Normal. Для того чтобы стало понятнее влияние приоритетов на исполнение потоков, об ратимся к примеру, в котором выполняются два потока: один с более высоким при оритетом. Оба потока создаются в качестве экземпляров объектов класса MyThread. В методе Run() организуется цикл, в котором подсчитывается определенное число повторений. Цикл завершается, когда подсчет достигает величины 1000000000 или ког да статическая переменная stop получает логическое значение true. Первоначально переменная stop получает логическое значение false. В первом потоке, где произ водится подсчет до 1000000000, устанавливается логическое значение true перемен ной stop. В силу этого второй поток оканчивается на следующем своем интервале времени. На каждом шаге цикла строка в переменной currentName проверяется на наличие имени исполняемого потока. Если имена потоков не совпадают, это означа ет, что произошло переключение исполняемых задач. Всякий раз, когда происходит переключение задач, имя нового потока отображается и присваивается переменной currentName. Это дает возможность отследить частоту доступа потока к ЦП. По окон чании обоих потоков отображается число повторений цикла в каждом из них. // Продемонстрировать влияние приоритетов потоков. using System; using System.Threading; class MyThread { public int Count; public Thread Thrd; static bool stop = false; static string currentName; / Сконструировать новый поток. Обратите внимание на то, что данный конструктор еще не начинает выполнение потоков. / public MyThread(string name) { Count = 0; Thrd = new Thread(this.Run); Thrd.Name = name; currentName = name; } // Начать выполнение нового потока. void Run() { Console.WriteLine("Поток " + Thrd.Name + " начат."); do { Count++; if(currentName != Thrd.Name) { currentName = Thrd.Name; Console.WriteLine("В потоке " + currentName); } } while(stop == false && Count < 1000000000); stop = true; Console.WriteLine("Поток " + Thrd.Name + " завершен."); } } class PriorityDemo { static void Main() { MyThread mt1 = new MyThread("с высоким приоритетом"); MyThread mt2 = new MyThread("с низким приоритетом"); // Установить приоритеты для потоков. mt1.Thrd.Priority = ThreadPriority.AboveNormal; mt2.Thrd.Priority = ThreadPriority.BelowNormal; // Начать потоки. mt1.Thrd.Start(); mt2.Thrd.Start(); mt1.Thrd.Join(); mt2.Thrd.Join(); Console.WriteLine(); Console.WriteLine("Поток " + mt1.Thrd.Name + " досчитал до " + mt1.Count); Console.WriteLine("Поток " + mt2.Thrd.Name + " досчитал до " + mt2.Count); } } Вот к какому результату может привести выполнение этой программы. Поток с высоким приоритетом начат. В потоке с высоким приоритетом Поток с низким приоритетом начат. В потоке с низким приоритетом В потоке с высоким приоритетом В потоке с низким приоритетом В потоке с высоким приоритетом В потоке с низким приоритетом В потоке с высоким приоритетом В потоке с низким приоритетом В потоке с высоким приоритетом В потоке с низким приоритетом В потоке с высоким приоритетом Поток с высоким приоритетом завершен. Поток с низким приоритетом завершен. Поток с высоким приоритетом досчитал до 1000000000 Поток с низким приоритетом досчитал до 23996334 Судя по результату, высокоприоритетный поток получил около 98% всего времени, которое было выделено для выполнения этой программы. Разумеется, конкретный ре зультат может отличаться в зависимости от быстродействия ЦП и числа других задач, решаемых в системе, а также от используемой версии Windows. Многопоточный код может вести себя по-разному в различных средах, поэтому никогда не следует полагаться на результаты его выполнения только в одной среде. Так, было бы ошибкой полагать, что низкоприоритетный поток из приведенного выше примера будет всегда выполняться лишь в течение небольшого периода времени до тех пор, пока не завершится высокоприоритетный поток. В другой среде высокоприо ритетный поток может, например, завершиться еще до того, как низкоприоритетный поток выполнится хотя бы один раз. Синхронизация Когда используется несколько потоков, то иногда приходится координировать дей ствия двух или более потоков. Процесс достижения такой координации называется синхронизацией. Самой распространенной причиной применения синхронизации слу жит необходимость разделять среди двух или более потоков общий ресурс, который может быть одновременно доступен только одному потоку. Например, когда в одном потоке выполняется запись информации в файл, второму потоку должно быть запре щено делать это в тот же самый момент времени. Синхронизация требуется и в том случае, если один поток ожидает событие, вызываемое другим потоком. В подобной ситуации требуются какие-то средства, позволяющие приостановить один из потоков до тех пор, пока не произойдет событие в другом потоке. После этого ожидающий по ток может возобновить свое выполнение. В основу синхронизации положено понятие блокировки, посредством которой ор ганизуется управление доступом к кодовому блоку в объекте. Когда объект заблокиро ван одним потоком, остальные потоки не могут получить доступ к заблокированному кодовому блоку. Когда же блокировка снимается одним потоком, объект становится доступным для использования в другом потоке. Средство блокировки встроено в язык С#. Благодаря этому все объекты могут быть синхронизированы. Синхронизация организуется с помощью ключевого слова lock. Она была предусмотрена в C# с самого начала, и поэтому пользоваться ею намного проще, чем кажется на первый взгляд. В действительности синхронизация объектов во многих программах на С# происходит практически незаметно. Ниже приведена общая форма блокировки: lock(lockObj) { // синхронизируемые операторы } где lockObj обозначает ссылку на синхронизируемый объект. Если же требуется син хронизировать только один оператор, то фигурные скобки не нужны. Оператор lock гарантирует, что фрагмент кода, защищенный блокировкой для данного объекта, бу дет использоваться только в потоке, получающем эту блокировку. А все остальные по токи блокируются до тех пор, пока блокировка не будет снята. Блокировка снимается по завершении защищаемого ею фрагмента кода. Блокируемым считается такой объект, который представляет синхронизируемый ресурс. В некоторых случаях им оказывается экземпляр самого ресурса или же про извольный экземпляр объекта, используемого для синхронизации. Следует, однако, иметь в виду, что блокируемый объект не должен быть общедоступным, так как в про тивном случае он может быть заблокирован из другого, неконтролируемого в про грамме фрагмента кода и в дальнейшем вообще не разблокируется. В прошлом для блокировки объектов очень часто применялась конструкция lock(this). Но она при годна только в том случае, если this является ссылкой на закрытый объект. В связи с возможными программными и концептуальными ошибками, к которым может при вести конструкция lock(this), применять ее больше не рекомендуется. Вместо нее лучше создать закрытый объект, чтобы затем заблокировать его. Именно такой подход принят в примерах программ, приведенных далее в этой главе. Но в унаследованном коде C# могут быть обнаружены примеры применения конструкции lock(this). В одних случаях такой код оказывается безопасным, а в других — требует изменений во избежание серьезных осложнений при его выполнении. В приведенной ниже программе синхронизация демонстрируется на примере управления доступом к методу SumIt(), суммирующему элементы целочисленного массива. // Использовать блокировку для синхронизации доступа к объекту. using System; using System.Threading; class SumArray { int sum; object lockOn = new object(); // закрытый объект, доступный // для последующей блокировки public int SumIt(int[] nums) { lock(lockOn) { // заблокировать весь метод sum = 0; // установ