Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
Глава 21 Многопоточное программирование 576 Часть II. Библиотека C# реди множества новых средств C# самым значительным, пожалуй, является встроенная поддержка многопоточного программирования (multithreaded programming). Многопоточная программа состоит из двух или больше частей, которые могут выполняться одновременно. Каждая часть такой программы называется потоком (thread), и каждый поток определяет собственный путь выполнения инструкций. Таким образом, многопоточность представляет собой специальную форму многозадачности. Многопоточное программирование опирается на сочетание средств, предусмотренных языком C#, и классов, определенных в среде .NET Framework. Многие проблемы, связанные с многопоточностью, которые имеют место в других языках, в C# минимизированы или устранены совсем, поскольку поддержка многопоточности встроена в язык. Основы многопоточности Различают два вида многозадачности: с ориентацией на процессы и с ориентацией на потоки. Важно понимать различие между ними. Процесс по сути представляет собой выполняемую программу. Следовательно, многозадачность, ориентированная на процессы, — это средство, позволяющее компьютеру выполнять две или больше программ одновременно. Например, именно благодаря многозадачности, ориентированной на процессы, мы можем работать с текстовым редактором (или с электронными таблицами) и в то же самое время искать нужную информацию в Internet. В процессно-ориентированной многозадачности программа является наименьшим элементом кода, которым может манипулировать планировщик задач. Поток — это управляемая единица выполняемого кода. В многозадачной среде, ориентированной на потоки, все процессы имеют по крайней мере один поток, но возможно и большее их количество. Это означает, что одна программа может выполнять сразу две и более задач. Например, текстовый редактор может форматировать текст и в то же время выводить что-либо на печать, поскольку эти два действия выполняются двумя отдельными потоками. Итак, различие между процессно- и поточно-ориентированной многозадачностями можно определить следующим образом. Процессно-ориентированная многозадачность обеспечивает одновременное выполнение программ, а поточно-ориентированная — одновременное выполнение частей одной и той же программы. Преимущество многопоточности состоит в том, что она позволяет писать очень эффективные программы, поскольку предоставляет возможность с толком использовать вынужденное время ожидания (простоя), которое имеет место во многих программах. Общеизвестно, что большинство устройств ввода-вывода (сетевые порты, дисководы или клавиатура) работают гораздо медленнее, чем центральный процессор (ЦП). Поэтому в программах львиная доля времени выполнения зачастую тратится на ожидание окончания отправки информации устройству (или получения от него). Используя многопоточность, можно построить программу так, чтобы она в такие периоды ожидания выполняла другую задачу. Например, пока одна часть программы будет отправлять файл по электронной почте, другая ее часть может считывать входные данные с клавиатуры, а еще одна — буферизировать следующий блок данных для отправки в Internet. Поток может находиться в одном из нескольких возможных состояний. Он может выполняться. Он может быть готовым к выполнению (как только получит время ЦП). Выполняющийся поток может быть приостановлен, т.е. его выполнение временно прекращается. Позже оно может быть возобновлено. Поток может быть заблокирован в С Глава 21. Многопоточное программирование 577 ожидании необходимого ресурса. Наконец, поток может завершиться, и уж в этом случае его выполнение окончено и продолжению (возобновлению) не подлежит. В среде .NET Framework определено два типа потоков: высокоприоритетный (foreground) и низкоприоритетный, или фоновый (background). По умолчанию поток создается высокоприоритетным, но его тип можно изменить, т.е. сделать его фоновым. Единственное различие между высоко- и низкоприоритетным потоками состоит в том, что последний будет автоматически завершен, если все высокоприоритетные потоки в его процессе остановились. Поточно-ориентированная многозадачность не может обойтись без специального средства, именуемого синхронизацией, которое позволяет координировать выполнение потоков вполне определенными способами. В C# предусмотрена отдельная подсистема, посвященная синхронизации, ключевые средства которой здесь также рассматриваются. Все процессы имеют по крайней мере один поток управления, который обычно называется основным (main thread), поскольку именно с этого потока начинается выполнение программы. Таким образом, все приведенные ранее в этой книге примеры программ использовали основной поток. Из основного можно создать и другие потоки. Язык C# и среда .NET Framework поддерживают как процессно-, так и поточно- ориентированную многозадачность. Следовательно, используя C#, можно создавать как процессы, так и потоки, а затем ими эффективно управлять. При этом, чтобы создать новый процесс, потребуется написать небольшую программку, поскольку каждый процесс в значительной степени отделен от следующего. Здесь важно то, что C# обеспечивает поддержку многопоточности. Поскольку поддержка многопоточности является встроенной, C# значительно упрощает создание высокоэффективных многопоточных программ по сравнению с другими языками, например по сравнению с C++ (в который не встроена поддержка многопоточности). Классы, которые поддерживают многопоточное программирование, определены в пространстве имен System.Threading . Поэтому в начало любой многопоточной программы необходимо включить следующую инструкцию: using System.Threading; Класс Thread Многопоточная система C# встроена в класс Thread , который инкапсулирует поток управления. Класс Thread является sealed -классом, т.е. он не может иметь наследников. В классе Thread определен ряд методов и свойств для управления потоками. Наиболее употребляемые члены этого класса рассматриваются на протяжении этой главы. Создание потока Чтобы создать поток, необходимо создать объект типа Thread . В классе Thread определен следующий конструктор: public Thread(ThreadStart entryPoint ) Здесь параметр entryPoint содержит имя метода, который будет вызван, чтобы начать выполнение потока. Тип ThreadStart — это делегат, определенный в среде .NET Framework: public delegate void ThreadStart() 578 Часть II. Библиотека C# Итак, начальный метод должен иметь тип возвращаемого значения void и не принимать никаких аргументов. Выполнение созданного потока не начнется до тех пор, пока не будет вызван метод Start() , который определяется в классе Thread . Его определение выглядит так: public void Start() Начавшись, выполнение потока будет продолжаться до тех пор, пока не завершится метод, заданный параметром entryPoint . Поэтому после выхода из entryPoint -метода выполнение потока автоматически завершается. Если попытаться вызвать метод Start() для потока, запушенного на выполнение, будет сгенерировано исключение типа ThreadStateException . Не забывайте, что класс Thread определен в пространстве имен System.Threading Рассмотрим пример создания нового потока и начала его выполнения: // Создаем поток управления. 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 { public static void Main() { Console.WriteLine("Основной поток стартовал."); // Сначала создаем объект класса MyThread. MyThread mt = new MyThread("Потомок #1"); // Затем из этого объекта создаем поток. Thread newThrd = new Thread(new ThreadStart(mt.run)); // Наконец, запускаем выполнение потока. newThrd.Start(); do { Console.Write("."); Глава 21. Многопоточное программирование 579 Thread.Sleep(10); } while(mt.count != 10); Console.WriteLine("Основной поток завершен."); } } Рассмотрим внимательно эту программу. Класс MyThread используется для создания второго потока управления. В его методе run() организован цикл, который “считает” от 0 до 9. Обратите внимание на вызов метода Sleep() , который является статическим и определен в классе Thread . Метод Sleep() заставляет поток, из которого он был вызван, приостановить выполнение на период времени, заданный в миллисекундах. В нашей программе проиллюстрирован следующий формат использования этого метода: public static void Sleep(int milliseconds ) В параметре milliseconds задается время в миллисекундах, на которое будет приостановлено выполнение потока. Если параметр milliseconds равен нулю, вызывающий поток приостанавливается только для того, чтобы ожидающему потоку разрешить выполнение. В методе Main() при выполнении следующих инструкций создается новый объект класса Thread : // Сначала создаем объект класса MyThread. MyThread mt = new MyThread("Потомок #1"); // Затем из этого объекта создаем поток. Thread newThrd = new Thread(new ThreadStart(mt.run)); // Наконец, запускаем выполнение потока. newThrd.Start(); Как подсказывают комментарии, сначала создается объект класса MyThread . Этот объект затем используется для создания объекта класса Thread путем передачи значения mt.run в качестве имени стартового метода, или метода входной точки. Наконец, вызов метода Start() запускает новый поток на выполнение. Это приводит к вызову метода run() дочернего потока. После вызова метода Start() выполнение основного потока возвращается в метод Main() , а именно в цикл do . Теперь выполняются оба потока, разделяя время ЦП до тех пор, пока не закончатся их циклы. Программа генерирует такие результаты: Основной поток стартовал. .Потомок #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 завершен. Основной поток завершен. Часто в многопоточной программе нужно позаботиться о том, чтобы основной поток завершался последним. Формально программа продолжает выполняться до тех пор, пока не завершатся все высокоприоритетные потоки. Таким образом, совсем не 580 Часть II. Библиотека C# обязательно завершение основного потока последним. Однако добиваться этого — считается одним из признаков хорошего стиля программирования, поскольку в этом случае ясно определяется конечная точка программы. В предыдущем примере основной поток гарантированно завершается последним, поскольку цикл do останавливается, когда значение переменной count становится равным 10 . Поскольку переменная count примет значение 10 только после того, как завершится выполнение потокового объекта newThrd , основной поток закончится последним. Ниже в этой главе будут показаны более удачные способы ожидания одним потоком завершения другого. А если немного усовершенствовать Несмотря на то что предыдущая программа вполне работоспособна, несколько простых усовершенствований сделают ее более эффективной. Во-первых, можно организовать начало выполнения потока сразу после его создания. Для потока класса MyThread это достижимо посредством создания объекта типа Thread внутри конструктора класса MyThread . Во-вторых, не обязательно хранить имя потока в классе MyThread , поскольку в классе Thread определено свойство Name , которое можно использовать с этой целью. Свойство 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(new ThreadStart(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 { Глава 21. Многопоточное программирование 581 public 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(new ThreadStart(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 { public static void Main() { 582 Часть II. Библиотека C# 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 завершен. Основной поток завершен. Глава 21. Многопоточное программирование 583 Как можно судить по приведенным результатам, сразу после старта все три потока разделяют время ЦП. Из-за различий в системных конфигурациях, операционных системах и других факторах среды результаты выполнения этой программы на вашем компьютере могут незначительно отличаться от представленных здесь. Как определить, завершено ли выполнение потока Иногда полезно знать, когда завершится выполнение потока. В предыдущих примерах это достигалось за счет проверки значения переменной count , но такое решение вряд ли можно считать удовлетворительным. К счастью, в классе Thread предусмотрено два средства, которые позволяют установить факт завершения выполнения потока. Одно из них — предназначенное только для чтения свойство IsAlive . Оно определяется так: public bool IsAlive { get; } Свойство IsAlive возвращает значение true , если поток, для которого оно опрашивается, еще выполняется. В противном случае оно возвращает значение false Чтобы опробовать эту возможность, замените следующей версией класса MoreThreads ту, что представлена в предыдущей программе: // Используем свойство IsAlive для установления факта // завершения выполнения потока. class MoreThreads { public 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("Основной поток завершен."); } } С использованием этой версии класса MoreThreads результаты работы программы аналогичны предыдущим. Единственное различие между этими версиями состоит в том, что в последней для установления факта завершения выполнения дочерних потоков используется свойство IsAlive Второй способ, которые позволяет “дождаться” завершения выполнения потока, состоит в вызове метода Join() . Самый простой формат его использования имеет такой вид: public void Join() Метод Join() ожидает, пока поток, для которого он был вызван, не завершится. Имя этого метода (“join” в перев. с англ. — присоединяться) связано с идеей вызвать |