Выбрать главу
zedThreadStart метода 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; // установить исходное значение суммы for(int i=0; i < nums.Length; i++) { sum += nums[i]; Console.WriteLine("Текущая сумма для потока " + Thread.CurrentThread.Name + " равна " + sum); Thread.Sleep(10); // разрешить переключение задач } return sum; } } } class MyThread { 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(this.Run); Thrd.Name = name; Thrd.Start(); // начать поток } // Начать выполнение нового потока. void Run() { Console.WriteLine(Thrd.Name + " начат."); answer = sa.SumIt(a); Console.WriteLine("Сумма для потока " + Thrd.Name + " равна " + answer); Console.WriteLine(Thrd.Name + " завершен."); } } class Sync { 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(); } } Ниже приведен результат выполнения данной программы, хотя у вас он может ока заться несколько иным. Потомок #1 начат. Текущая сумма для потока Потомок #1 равна 1 Потомок #2 начат. Текущая сумма для потока Потомок #1 равна 3 Текущая сумма для потока Потомок #1 равна 6 Текущая сумма для потока Потомок #1 равна 10 Текущая сумма для потока Потомок #1 равна 15 Текущая сумма для потока Потомок #2 равна 1 Сумма для потока Потомок #1 равна 15 Потомок #1 завершен. Текущая сумма для потока Потомок #2 равна 3 Текущая сумма для потока Потомок #2 равна 6 Текущая сумма для потока Потомок #2 равна 10 Текущая сумма для потока Потомок #2 равна 15 Сумма для потока Потомок #2 равна 15 Потомок #2 завершен. Как следует из приведенного выше результата, в обоих потоках правильно подсчи тывается сумма, равная 15. Рассмотрим эту программу более подробно. Сначала в ней создаются три класса. Первым из них оказывается класс SumArray, в котором определяется метод SumIt(), суммирующий элементы целочисленного массива. Вторым создается класс MyThread, в котором используется статический объект sa типа SumArray. Следовательно, един ственный объект типа SumArray используется всеми объектами типа MyThread. С по мощью этого объекта получается сумма элементов целочисленного массива. Обратите внимание на то, что текущая сумма запоминается в поле sum объекта типа SumArray. Поэтому если метод SumIt() используется параллельно в двух потоках, то оба потока попытаются обратиться к полю sum, чтобы сохранить в нем текущую сумму. А по скольку это может привести к ошибкам, то доступ к методу SumIt() должен быть синхронизирован. И наконец, в третьем классе, Sync, создаются два потока, в которых подсчитывается сумма элементов целочисленного массива. Оператор lock в методе SumIt() препятствует одновременному использованию данного метода в разных потоках. Обратите внимание на то, что в операторе lock объ ект lockOn используется в качестве синхронизируемого. Это закрытый объект, предна значенный исключительно для синхронизации. Метод Sleep() намеренно вызывается для того, чтобы произошло переключение задач, хотя в данном случае это невозмож но. Код в методе SumIt() заблокирован, и поэтому он может быть одновременно ис пользован только в одном потоке. Таким образом, когда начинает выполняться второй порожденный поток, он не сможет войти в метод SumIt() до тех пор, пока из него не выйдет первый порожденный поток. Благодаря этому гарантируется получение пра вильного результата. Для того чтобы полностью уяснить принцип действия блокировки, попробуйте удалить из рассматриваемой здесь программы тело метода SumIt(). В итоге метод SumIt() перестанет быть синхронизированным, а следовательно, он может парал лельно использоваться в любом числе потоков для одного и того же объекта. Посколь ку текущая сумма сохраняется в поле sum, она может быть изменена в каждом потоке, вызывающем метод SumIt(). Это означает, что если два потока одновременно вызы вают метод SumIt() для одного и того же объекта, то конечный результат получается неверным, поскольку содержимое поля sum отражает смешанный результат сумми рования в обоих потоках. В качестве примера ниже приведен результат выполнения рассматриваемой здесь программы после снятия блокировки с метода SumIt(). Потомок #1 начат. Текущая сумма для потока Потомок #1 равна 1 Потомок #2 начат. Текущая сумма для потока Потомок #2 равна 1 Текущая сумма для потока Потомок #1 равна 3 Текущая сумма для потока Потомок #2 равна 5 Текущая сумма для потока Потомок #1 равна 8 Текущая сумма для потока Потомок #2 равна 11 Текущая сумма для потока Потомок #1 равна 15 Текущая сумма для потока Потомок #2 равна 19 Текущая сумма для потока Потомок #1 равна 24 Текущая сумма для потока Потомок #2 равна 29 Сумма для потока Потомок #1 равна 29 Потомок #1 завершен. Текущая сумма для потока Потомок #2 равна 29 Потомок #2 завершен. Как следует из приведенного выше результата, в обоих порожденных потоках ме тод SumIt() используется одновременно для одного и того же объекта, а это приводит к искажению значения в поде sum. Ниже подведены краткие итоги использования блокировки. • Если блокировка любого заданного объекта получена в одном потоке, то после блокировки объекта она не может быть получена в другом потоке. • Остальным потокам, пытающимся получить блокировку того же самого объек та, придется ждать до тех пор, пока объект не окажется в разблокированном состоянии. • Когда поток выходит из заблокированного фрагмента кода, соответствующий объект разблокируется. Другой подход к синхронизации потоков Несмотря на всю простоту и эффективность блокировки кода метода, как показано в приведенном выше примере, такое средство синхронизации оказывается пригодным далеко не всегда. Допустим, что требуется синхронизировать доступ к методу класса, который был создан кем-то другим и сам не синхронизирован. Подобная ситуация вполне возможна при использовании чужого класса, исходный код которого недо ступен. В этом случае оператор lock нельзя ввести в соответствующий метод чужого класса. Как же тогда синхронизировать объект такого класса? К счастью, этот вопрос разрешается довольно просто: доступ к объекту может быть заблокирован из внеш него кода по отношению к данному объекту, для чего достаточно указать этот объ ект в операторе lock. В качестве примера ниже приведен другой вариант реализации предыдущей программы. Обратите внимание на то, что код в методе SumIt() уже не является заблокированным, а объект lockOn больше не объявляется. Вместо этого вы зовы метода SumIt() блокируются в классе MyThread. // Другой способ блокировки для синхронизации доступа к объекту. 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 { 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(this.Run); Thrd.Name = name; Thrd.Start(); // начать поток } // Начать выполнение нового потока. void Run() { Console.WriteLine(Thrd.Name + " начат."); // Заблокировать вызовы метода SumIt(). lock(sa) answer = sa.SumIt(a); Console.WriteLine("Сумма для потока " + Thrd.Name + " равна " + answer); Console.WriteLine(Thrd.Name + " завершен."); } } class Sync { 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(). Ниже приведена соответствующая строка кода, в которой осуществляется подобная блокировка. // Заблокировать вызовы метода SumIt(). lock(sa) answer = sa.SumIt(а); Объект sa является закрытым, и поэтому он может быть благополучно заблокиро ван. При таком подходе к синхронизации потоков данная программа дает такой же правильный результат, как и при первоначальном подходе. Класс Monitor и блокировка Ключевое слово lock на самом деде служит в C# быстрым способом доступа к средствам синхронизации, определенным в классе Monitor, который находится в про странстве имен System.Threading. В этом классе определен, в частности, ряд методов для управления синхронизацией. Например, для получения блокировки объекта вы зывается метод Enter(), а для снятия блокировки — метод Exit(). Ниже приведены общие формы этих методов: public static void Enter(object obj) public static void Exit(object obj) где obj обозначает синхронизируемый объект. Если же объект недоступен, то после вызова метода Enter() вызывающий поток ожидает до тех пор, пока объект не станет доступным. Тем не менее методы Enter() и Exit() применяются редко, поскольку оператор lock автоматически предоставляет эквивалентные средства синхронизации потоков. Именно поэтому оператор lock оказывается "более предпочтительным" для получения блокировки объекта при программировании на С#. Впрочем, один метод из класса Monitor может все же оказаться полезным. Это метод TryEnter(), одна из общих форм которого приведена ниже. public static bool TryEnter(object obj) Этот метод возвращает логическое значение true, если вызывающий поток полу чает блокировку для объекта obj, а иначе он возвращает логическое значение false. Но в любом случае вызывающему потоку придется ждать своей очереди. С помощью метода TryEnter() можно реализовать альтернативный вариант синхронизации по токов, если требуемый объект временно недоступен. Кроме того, в классе Monitor определены методы Wait(), Pulse() и PulseAll(), которые рассматриваются в следующем разделе. Сообщение между потоками с помощью методов Wait(), Pulse() и PulseAll() Рассмотрим следующую ситуацию. Поток Т выполняется в кодовом блоке lock, и ему требуется доступ к ресурсу R, который временно недоступен. Что же тогда делать потоку Т? Если поток Т войдет в организованный в той или иной форме цикл опроса, ожидая освобождения ресурса R, то тем самым он свяжет соответствующий объект, блокируя доступ к нему других потоков. Это далеко не самое оптимальное решение, поскольку оно лишает отчасти преимуществ программирования для многопоточной среды. Более совершенное решение заключается в том, чтобы временно освободить объект и тем самым