Главная страница
Навигация по странице:

  • Основы многопоточности

  • Класс Thread

  • Создание потока

  • А если немного усовершенствовать

  • Создание нескольких потоков

  • Как определить, завершено ли выполнение потока

  • Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией


    Скачать 5.05 Mb.
    НазваниеСправочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
    АнкорC #.pdf
    Дата08.12.2017
    Размер5.05 Mb.
    Формат файлаpdf
    Имя файлаC #.pdf
    ТипСправочник
    #10795
    страница38 из 52
    1   ...   34   35   36   37   38   39   40   41   ...   52
    Глава 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” в перев. с англ. — присоединяться) связано с идеей вызвать

    1   ...   34   35   36   37   38   39   40   41   ...   52


    написать администратору сайта