Полное руководство. С# 4.0 - Шилдт Герберт
Шрифт:
Интервал:
Закладка:
Различают две разновидности многозадачности: на основе процессов и на основепотоков. В связи с этим важно понимать отличия между ними. Процесс фактическипредставляет собой исполняемую программу. Поэтому многозадачность на основе процессов — это средство, благодаря которому на компьютере могут параллельно выполняться две программы и более. Так, многозадачность на основе процессов позволяетодновременно выполнять программы текстового редактора, электронных таблиц ипросмотра содержимого в Интернете. При организации многозадачности на основепроцессов программа является наименьшей единицей кода, выполнение которой может координировать планировщик задач.Поток представляет собой координируемую единицу исполняемого кода. Своимпроисхождением этот термин обязан понятию "поток исполнения". При организациимногозадачности на основе потоков у каждого процесса должен быть по крайней мереодин поток, хотя их может быть и больше. Это означает, что в одной программе одновременно могут решаться две задачи и больше. Например, текст может форматироваться в редакторе текста одновременно с его выводом на печать, при условии, что обаэти действия выполняются в двух отдельных потоках.Отличия в многозадачности на основе процессов и потоков могут быть сведены кследующему: многозадачность на основе процессов организуется для параллельноговыполнения программ, а многозадачность на основе потоков — для параллельного выполнения отдельных частей одной программы.Главное преимущество многопоточной обработки заключается в том, что она позволяет писать программы, которые работают очень эффективно благодаря возможности выгодно использовать время простоя, неизбежно возникающее в ходе выполнениябольшинства программ. Как известно, большинство устройств ввода-вывода, будь тоустройства, подключенные к сетевым портам, накопители на дисках или клавиатура,работают намного медленнее, чем центральный процессор (ЦП). Поэтому большуючасть своего времени программе приходится ожидать отправки данных на устройствоввода-вывода или приема информации из него. А благодаря многопоточной обработке программа может решать какую-нибудь другую задачу во время вынужденного простоя. Например, в то время как одна часть программы отправляет файл черезсоединение с Интернетом, другая ее часть может выполнять чтение текстовой информации, вводимой с клавиатуры, а третья — осуществлять буферизацию очередногоблока отправляемых данных.Поток может находиться в одном из нескольких состояний. В целом, поток можетбыть выполняющимся; готовым к выполнению, как только он получит время и ресурсыЦП; приостановленным, т:е. временно не выполняющимся; возобновленным в дальнейшем; заблокированным в ожидании ресурсов для своего выполнения; а также завершенным, когда его выполнение окончено и не может быть возобновлено.В среде .NET Framework определены две разновидности потоков: приоритетныйи фоновый. По умолчанию создаваемый поток автоматически становится приоритетным, но его можно сделать фоновым. Единственное отличие приоритетных потоков отфоновых заключается в том, что фоновый поток автоматически завершается, если в егопроцессе остановлены все приоритетные потоки.В связи с организацией многозадачности на основе потоков возникает потребностьв особого рода режиме, который называется синхронизацией и позволяет координировать выполнение потоков вполне определенным образом. Для такой синхронизациив C# предусмотрена отдельная подсистема, основные средства которой рассматриваются в этой главе.Все процессы состоят хотя бы из одного потока, который обычно называют основным, поскольку именно с него начинается выполнение программы. Следовательно,в основном потоке выполнялись все приведенные ранее примеры программ. Из основного потока можно создать другие потоки.В языке C# и среде .NET Framework поддерживаются обе разновидности многозадачности: на основе процессов и на основе потоков. Поэтому средствами C# можносоздавать как процессы, так и потоки, а также управлять и теми и другими. Длятого чтобы начать новый процесс, от программирующего требуется совсем немного усилий, поскольку каждый предыдущий процесс совершенно обособлен от последующего. Намного более важной оказывается поддержка в C# многопоточнойобработки, благодаря которой упрощается написание высокопроизводительных,многопоточных программ на C# по сравнению с некоторыми другими языками программирования.Классы, поддерживающие многопоточное программирование, определены в пространстве имен System.Threading. Поэтому любая многопоточная программа на C#включает в себя следующую строку кода.using System.Threading;Класс ThreadСистема многопоточной обработки основывается на классе Thread, который инкапсулирует поток исполнения. Класс Thread является герметичным, т.е. он не можетнаследоваться. В классе Thread определен ряд методов и свойств, предназначенныхдля управления потоками. На протяжении всей этой главы будут рассмотрены наиболее часто используемые члены данного класса.Создание и запуск потокаДля создания потока достаточно получить экземпляр объекта типа Thread,т.е. класса, определенного в пространстве имен System.Threading. Ниже приведенапростейшая форма конструктора класса Thread:public Thread(ThreadStart запуск)где запуск — это имя метода, вызываемого с целью начать выполнение потока,a ThreadStart — делегат, определенный в среде .NET Framework, как показанониже.public delegate void ThreadStart()Следовательно, метод, указываемый в качестве точки входа в поток, должен иметьвозвращаемый тип void и не принимать никаких аргументов.Вновь созданный новый поток не начнет выполняться до тех пор, пока не будетвызван его метод Start(), определяемый в классе Thread. Существуют две формыобъявления метода Start(). Ниже приведена одна из них.public void Start()Однажды начавшись, поток будет выполняться до тех пор, пока не произойдетвозврат из метода, на который указывает запуск. Таким образом, после возврата изэтого метода поток автоматически прекращается. Если же попытаться вызвать методStart() для потока, который уже начался, это приведет к генерированию исключения ThreadStateException.В приведенном ниже примере программы создается и начинает выполняться новый поток.// Создать поток исполнения.using System;using System.Threading;class MyThread {public int Count;string thrdName;public MyThread(string name) {Count = 0;thrdName = name;}// Точка входа в поток.public void Run() {Console.WriteLine(thrdName + " начат.");do {Thread.Sleep(500);Console.WriteLine("В потоке " + thrdName + ", Count = " + Count);Count++;} while(Count < 10);Console.WriteLine(thrdName + " завершен.");}}class MultiThread {static void Main() {Console.WriteLine("Основной поток начат.");// Сначала сконструировать объект типа MyThread.MyThread mt = new MyThread("Потомок #1");// Далее сконструировать поток из этого объекта.Thread newThrd = new Thread(mt.Run);// И наконец, начать выполнение потока.newThrd.Start();do {Console.Write(".");Thread.Sleep(100);} while (mt.Count != 10);Console.WriteLine("Основной поток завершен.");}}Рассмотрим приведенную выше программу более подробно. В самом ее началеопределяется класс MyThread, предназначенный для создания второго потока исполнения. В методе Run() этого класса организуется цикл для подсчета от 0 до 9. Обратите внимание на вызов статического метода Sleep(), определенного в классе Thread.Этот метод обусловливает приостановление того потока, из которого он был вызван,на определенный период времени, указываемый в миллисекундах. Когда приостанавливается один поток, может выполняться другой. В данной программе используетсяследующая форма метода Sleep():public static void Sleep(int миллисекундпростоя)где миллисекундпростоя обозначает период времени, на который приостанавливается выполнение потока. Если указанное количество миллисекундпростоя равнонулю, то вызывающий поток приостанавливается лишь для того, чтобы предоставитьвозможность для выполнения потока, ожидающего своей очереди.В методе Main() новый объект типа Thread создается с помощью приведеннойниже последовательности операторов.// Сначала сконструировать объект типа MyThread.MyThread mt = new MyThread("Потомок #1");// Далее сконструировать поток из этого объекта.Thread newThrd = new Thread(mt.Run);// И наконец, начать выполнение потока.newThrd.Start();Как следует из комментариев к приведенному выше фрагменту кода, сначала создается объект типа MyThread. Затем этот объект используется для создания объектатипа Thread, для чего конструктору этого объекта в качестве точки входа передаетсяметод mt.Run(). И наконец, выполнение потока начинается с вызова метода Start().Благодаря этому метод mt.Run() выполняется в своем собственном потоке. После вызова метода Start() выполнение основного потока возвращается к методу Main(),где начинается цикл do-while. Оба потока продолжают выполняться, совместно используя ЦП, вплоть до окончания цикла. Ниже приведен результат выполнения данной программы. (Он может отличаться в зависимости от среды выполнения, операционной системы и степени загрузки задач.)Основной поток начат.Потомок #1 начат.....В потоке Потомок #1, Count = 0....В потоке Потомок #1, Count = 1....В потоке Потомок #1, Count = 2....В потоке Потомок #1, Count = 3....В потоке Потомок #1, Count = 4....В потоке Потомок #1, Count = 5....В потоке Потомок #1, Count = 6....В потоке Потомок #1, Count = 7....В потоке Потомок #1, Count = 8....В потоке Потомок #1, Count = 9Потомок #1 завершен.Основной поток завершен.Зачастую в многопоточной программе требуется, чтобы основной поток был последним потоком, завершающим ее выполнение. Формально программа продолжаетвыполняться до тех пор, пока не завершатся все ее приоритетные потоки. Поэтомутребовать, чтобы основной поток завершал выполнение программы, совсем не обязательно. Тем не менее этого правила принято придерживаться в многопоточномпрограммировании, поскольку оно явно определяет конечную точку программы.В рассмотренной выше программе предпринята попытка сделать основной потокзавершающим ее выполнение. Для этой цели значение переменной Count проверяется в цикле do-while внутри метода Main(), и как только это значение оказываетсяравным 10, цикл завершается и происходит поочередный возврат из методов Sleep().Но такой подход далек от совершенства, поэтому далее в этой главе будут представлены более совершенные способы организации ожидания одного потока до завершениядругого.Простые способы усовершенствования многопоточной программыРассмотренная выше программа вполне работоспособна, но ее можно сделать более эффективной, внеся ряд простых усовершенствований. Во-первых, можно сделатьтак, чтобы выполнение потока начиналось сразу же после его создания. Для этого достаточно получить экземпляр объекта типа Thread в конструкторе класса MyThread.И во-вторых, в классе MyThread совсем не обязательно хранить имя потока, посколькудля этой цели в классе Thread специально определено свойство Name.public string Name { get; set; }Свойство Name доступно для записи и чтения и поэтому может сложить как длязапоминания, так и для считывания имени потока.Ниже приведена версия предыдущей программы, в которую внесены упомянутыевыше усовершенствования.// Другой способ запуска потока.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 MultiThreadImproved {static void Main() {Console.WriteLine("Основной поток начат.");// Сначала сконструировать объект типа MyThread.MyThread mt = new MyThread("Потомок #1");do {Console.Write(".");Thread.Sleep(100);} while (mt.Count != 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.HighestThreadPriority.AboveNormalThreadPriority.NormalThreadPriority.BelowNormalThreadPriority.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 нельзя ввести в соответствующий метод чужогокласса. Как же тогда синхронизировать объект такого класса? К счастью, этот вопросразрешается довольно просто: доступ к объекту может быть заблокирован из внешнего кода по отношению к данному объекту, для чего до