программирование. Руководство su P# a n Reference в herbert schildt полное руководство с 0 герберт шилдт
Скачать 3.32 Mb.
|
ГЛАВА 23 Многопоточное программирование. Часть первая: основы С реди многих замечательных свойств языка C# особое место принадлежит поддержке многопоточного программирования. Многопоточная программа состоит из двух или более частей, выполняемых параллельно. Каждая часть такой программы называется потоком и определяет отдельный путь выполнения команд. Таким образом, многопоточная обработка является особой формой многозадачности. Многопоточное программирование опирается на целый ряд средств, предусмотренных для этой цели в самом языке С#, а также на классы, определенные в среде .NET Framework. Благодаря встроенной в C# поддержке многопоточной обработки сводятся к минимуму или вообще устраняются многие трудности, связанные с организацией многопоточной обработки в других языках программирования. Как станет ясно из дальнейшего, поддержка в C# многопоточной обработки четко организована и проста для понимания. С выпуском версии 4.0 в среде .NET Framework появились два важных дополнения, имеющих отношение к многопоточным приложениям. Первым из них является TPL (Task Parallel Library – Библиотека распараллеливания задач), а вторым – PLINQ (Parallel LINQ – Параллельный язык интегрированных запросов). Оба дополнения поддерживают параллельное программирование и позволяют использовать преимущества, предоставляемые многопроцессорными (многоядерными) компьютерами в отношении обработки данных. Кроме того, библиотека TPL упрощает создание многопоточных приложений и управление ими. В силу этого многопоточная обработка, опирающаяся на TPL, рекомендуется теперь как основной подход к разработке многопоточных приложений. Тем не менее накопленный опыт создания исходной многопоточной подсистемы по‑прежнему имеет значение по целому ряду причин. Во‑первых, уже существует немалый объем унаследованного кода, в котором применяется первоначальный подход к многопоточной обработке. Если приходится работать с таким кодом или сопровождать его, то нужно знать, как работает исходная многопоточная система. Во‑вторых, в коде, опирающемся на TPL, могут по‑прежнему использоваться элементы исходной многопоточной системы, и особенно ее средства синхронизации. И в‑третьих, несмотря на то что сама библиотека TPL основывается на абстракции, называемой задачей , она по‑прежнему неявно опирается на потоки и потоковые средства, описываемые в этой главе. Поэтому для полного усвоения и применения TPL потребуются твердые знания материала, излагаемого в этой главе. И наконец, следует особо подчеркнуть, что многопоточная обработка представляет собой довольно обширную тему, и поэтому подробное ее изложение выходит за рамки этой книги. В этой и последующей главах представлен лишь беглый обзор данной темы и демонстрируется ряд основополагающих методик. Следовательно, материал этих глав может служить введением в эту важную тему и основанием для дальнейшего ее самостоятельного изучения. Основы многопоточной обработки Различают две разновидности многозадачности: на основе процессов и на основе потоков. В связи с этим важно понимать отличия между ними. Процесс фактически представляет собой исполняемую программу. Поэтому многозадачность на основе процессов – это средство, благодаря которому на компьютере могут параллельно выполняться две программы и более. Так, многозадачность на основе процессов позволяет одновременно выполнять программы текстового редактора, электронных таблиц и просмотра содержимого в Интернете. При организации многозадачности на основе процессов программа является наименьшей единицей кода, выполнение которой может координировать планировщик задач. Поток представляет собой координируемую единицу исполняемого кода. Своим происхождением этот термин обязан понятию "поток исполнения'7. При организации многозадачности на основе потоков у каждого процесса должен быть по крайней мере один поток, хотя их может быть и больше. Это означает, что в одной программе одновременно могут решаться две задачи и больше. Например, текст может форматироваться в редакторе текста одновременно с его выводом на печать, при условии, что оба эти действия выполняются в двух отдельных потоках. Отличия в многозадачности на основе процессов и потоков могут быть сведены к следующему: многозадачность на основе процессов организуется для параллельного выполнения программ, а многозадачность на основе потоков – для параллельного выполнения отдельных частей одной программы. Главное преимущество многопоточной обработки заключается в том, что она позволяет писать программы, которые работают очень эффективно благодаря возможности выгодно использовать время простоя, неизбежно возникающее в ходе выполнения большинства программ. Как известно, большинство устройств ввода‑вывода, будь то устройства, подключенные к сетевым портам, накопители на дисках или клавиатура, работают намного медленнее, чем центральный процессор (ЦП). Поэтому большую часть своего времени программе приходится ожидать отправки данных на устройство ввода‑вывода или приема информации из него. А благодаря многопоточной обработке программа может решать какую‑нибудь другую задачу во время вынужденного простоя. Например, в то время как одна часть программы отправляет файл через соединение с Интернетом, другая ее часть может выполнять чтение текстовой информации, вводимой с клавиатуры, а третья – осуществлять буферизацию очередного блока отправляемых данных. Поток может находиться в одном из нескольких состояний. В целом, поток мол<:ет быть выполняющимся; готовым к выполнению, как только он получит время и ресурсы ЦП; приостановленным , т.е. временно не выполняющимся; возобновленным в дальнейшем; заблокированным в ожидании ресурсов для своего выполнения; а также завершенным, когда его выполнение окончено и не может быть возобновлено. В среде .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 проверяется в цикле do‑while внутри метода Main () , и как только это значение оказывается равным 10, цикл завершается и происходит поочередный возврат из методов Sleep (). Но такой подход далек от совершенства, поэтому далее в этой главе будут представлены более совершенные способы организации ожидания одного потока до завершения другого. Простые способы усовершенствования многопоточной программы Рассмотренная выше программа вполне работоспособна, но ее можно сделать более эффективной, внеся ряд простых усовершенствований, (to‑первых, можно сделать так, чтобы выполнение потока начиналось сразу же после его создания. Для этого достаточно получить экземпляр объекта типа 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 MultiThreadlmproved { 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() { |