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

  • Блокирование статического метода

  • Класс Monitor и инструкция lock

  • Взаимодействие потоков с помощью методов Wait(), Pulse() и PulseAll()

  • Пример использования методов Wait() и Pulse()

  • Использование атрибута MethodImplAttribute

  • Приостановка, возобновление и завершение выполнения потоков

  • Альтернативный формат использования метода Abort()

  • Отмена действия метода Abort()

  • Определение состояния потока

  • Использование основного потока

  • Запуск отдельной задачи

  • Справочник по 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
    страница40 из 52
    1   ...   36   37   38   39   40   41   42   43   ...   52
    592
    Часть II. Библиотека C#
    1. Если для заданного объекта инструкция lock размещена в некотором блоке кода, этот объект блокируется, и никакой другой поток не сможет запросить блокировку.
    2. Другие потоки, пытающиеся запросить блокировку для того же объекта, перейдут в состояние ожидания и будут находиться в нем до тех пор, пока код не будет разблокирован.
    3. Объект разблокируется, когда поток выходит из заблокированного блока.
    Необходимо также отметить, что инструкция lock должна использоваться только для объектов, которые определены либо как private-
    , либо как internal- объекты. В противном случае внешние по отношению к вашей программе потоки смогут получать блокировку и не снимать ее.
    Альтернативное решение
    Несмотря на то что блокирование кода метода, как показано в предыдущей программе, не представляет большой сложности и является эффективным средством синхронизации, оно, к сожалению, работает не во всех случаях. Например, нужно получить синхронизированный доступ к методу класса, который создавали не вы лично, а в нем самом средств синхронизации изначально не предусмотрено. Такая ситуация возможна в том случае, если вы хотите использовать класс, который написан сторонней организацией, и невозможно получить доступ к его исходному коду. В этом случае, очевидно, вам не удастся добавить инструкцию lock в соответствующий метод класса. Как тогда синхронизировать доступ к объекту такого класса? К счастью, есть очень простое решение описанной проблемы: заблокировать доступ к объекту из внешнего (по отношению к объекту) кода, указав этот объект в инструкции lock
    . Например, рассмотрим альтернативную реализацию предыдущей программы. Обратите внимание на то, что код в методе sumIt()
    больше не блокируется. Теперь блокируются обращения к методу sumIt()
    внутри класса
    MyThread
    // Еще один способ использовать инструкцию lock
    // для синхронизации доступа к объекту. 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 {

    Глава 21. Многопоточное программирование
    593 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(new ThreadStart(this.run)); thrd.Name = name; thrd.Start();
    //
    Запускаем поток на выполнение.
    }
    //
    Начало выполнения нового потока. void run() {
    Console.WriteLine(thrd.Name + " стартовал.");
    //
    Инструкция lock содержит вызов метода sumIt(). lock(sa) answer = sa.sumIt(a);
    Console.WriteLine("Сумма для потока " + thrd.Name +
    " равна " + answer);
    Console.WriteLine(thrd.Name + " завершен.");
    }
    } class Sync { public 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()
    . Это реализуется таким способом:
    // Инструкция lock содержит вызов метода sumIt(). lock(sa) answer = sa.sumIt(a);
    При выполнении этой программы получаем такие же корректные результаты, как и при использовании исходного решения.
    Блокирование статического метода
    Поскольку блокировка работает по отношению к объекту, то на первый взгляд может показаться, что невозможно заблокировать код static
    -метода, поскольку не существует объекта, для которого требуется выполнить блокировку. В действительности все обстоит иначе. Чтобы заблокировать static
    -метод, достаточно использовать инструкцию lock в следующем формате:

    594
    Часть II. Библиотека C# lock(typeof(
    class
    )) {
    //
    Блокируемый код.
    }
    Здесь
    class
    представляет собой имя класса, в котором содержится static
    -метод, подлежащий блокировке.
    Класс Monitor и инструкция lock
    Ключевое слово lock
    — это не что иное, как сокращенный вариант использования средств синхронизации, определенных в классе
    Monitor
    , принадлежащем пространству имен
    System.Threading
    . В классе
    Monitor определено несколько методов синхронизации. Например, чтобы получить возможность блокировки для некоторого объекта, вызовите метод
    Enter()
    , а чтобы снять блокировку — метод
    Exit()
    . Эти методы имеют следующий формат public static void Enter(object
    syncOb
    ) public static void Exit(object
    syncOb
    )
    Здесь
    syncOb
    — синхронизируемый объект. Если при вызове метода
    Enter()
    заданный объект недоступен, вызывающий поток будет ожидать до тех пор, пока объект не станет доступным. Разработчики из компании Microsoft утверждают, что lock
    -блок
    “совершенно эквивалентен” вызову метода
    Enter()
    с последующим вызовом метода
    Exit()
    . Но поскольку lock
    — это встроенная инструкция языка C#, то для получения блокировки в C#-программировании предпочтительнее использовать именно ее.
    Обратите внимание на метод
    TryEnter()
    из класса
    Monitor
    . Один из форматов его использования таков: public static bool TryEnter(object
    syncOb
    )
    Метод возвращает значение true, если вызывающий поток получает блокировку для объекта
    syncOb
    , и значение false в противном случае. Если заданный объект недоступен, вызывающий поток будет ожидать до тех пор, пока он не станет доступным.
    В классе
    Monitor определены еще три метода:
    Wait()
    ,
    Pulse()
    и
    PulseAll()
    Они описаны в следующем разделе.
    Взаимодействие потоков с помощью методов
    Wait(), Pulse() и PulseAll()
    Рассмотрим следующую ситуацию. Поток (назовем его
    T
    ) выполняет содержимое lock
    -блока и требует доступ к ресурсу (назовем его
    R
    ), который временно недоступен. Что делать потоку
    T
    ? Если поток
    T
    войдет в цикл опроса в ожидании доступности ресурса
    R
    , он свяжет объект, блокируя доступ к нему другим потокам. Это решение трудно назвать оптимальным, поскольку оно аннулирует преимущества программирования в многопоточной среде. Будет лучше, если поток
    T
    временно откажется от “претензий” на объект, позволив другому потоку выполнить свою работу. Когда же ресурс
    R
    станет доступным, поток
    T
    можно уведомить об этом, и он возобновит выполнение. Такой подход опирается на межпоточные средства общения, которые позволяют одному потоку уведомить другой о том, что он блокируется, а затем первого поставить в известность о том, что он может возобновить выполнение. C# поддерживает межпоточное взаимодействие с помощью методов
    Wait()
    ,
    Pulse() и
    PulseAll()
    Методы
    Wait()
    ,
    Pulse()
    и
    PulseAll()
    определены в классе
    Monitor
    . Эти методы можно вызывать только внутри lock
    -блока кода. Когда выполнение потока

    Глава 21. Многопоточное программирование
    595 временно блокируется, вызывается метод
    Wait()
    , т.е. он переходит в режим ожидания
    (“засыпает”) и снимает блокировку с объекта, позволяя другому потоку использовать этот объект. Позже, когда другой поток входит в аналогичное состояние блокирования и вызывает метод
    Pulse() или
    PulseAll()
    , “спящий” поток “просыпается”. Обращение к методу
    Pulse()
    возобновляет выполнение потока, стоящего первым в очереди потоков, пребывающих в режиме ожидания. Обращение к методу
    PulseAll()
    сообщает о снятии блокировки всем ожидающим потокам. Вот два наиболее употребимых формата метода
    Wait()
    : public static bool Wait(object
    waitOb
    ) public static bool Wait(object
    waitOb
    , int
    milliseconds
    )
    Первый формат означает ожидание до уведомления. Второй — подразумевает ожидание до уведомления или до истечения периода времени, заданного в миллисекундах.
    В обоих случаях параметр
    waitOb
    задает объект, к которому ожидается доступ.
    Форматы использования методов
    Pulse()
    и
    PulseAll()
    таковы: public static void Pulse(object
    waitOb
    ) public static void PulseAll(object
    waitOb
    )
    Здесь параметр
    waitOb
    означает объект, освобождаемый от блокировки. Если метод
    Wait()
    ,
    Pulse()
    или
    PulseAll()
    вызывается из кода, который находится вне lock
    - блока, генерируется исключение типа
    SynchronizationLockException
    Пример использования методов Wait() и Pulse()
    Чтобы понять необходимость применения методов
    Wait()
    и
    Pulse()
    , создадим программу, которая имитирует тиканье часов посредством отображения на экране слов
    “тик” и “так”. Для этого создадим класс
    TickTock
    , который содержит два метода: tick()
    и tock()
    . Метод tick()
    отображает слово “тик”, а метод tock()
    — слово “так”. Для работы нашего “часового механизма” создаем два потока, причем один из них будет вызывать метод tick()
    , а другой — метод tock()
    . Наша задача — организовать выполнение этих потоков таким образом, чтобы программа последовательно отображала
    “тик-так”, т.е. сначала слово “тик”, а за ним — слово “так”.
    // Использование методов Wait() и Pulse() для создания
    // тикающих часов. using System; using System.Threading; class TickTock { public void tick(bool running) { lock(this)
    { if(!running)
    {
    //
    Останов часов.
    Monitor.Pulse(this);
    //
    Уведомление любых
    // ожидающих потоков. return;
    }
    Console.Write("тик");
    Monitor.Pulse(this);
    //
    Разрешает выполнение
    // метода tock().
    Monitor.Wait(this);
    //
    Ожидаем завершения
    // метода tock().
    }

    596
    Часть II. Библиотека C#
    } public void tock(bool running) { lock(this)
    { if(!running)
    {
    //
    Останов часов.
    Monitor.Pulse(this);
    //
    Уведомление любых
    // ожидающих потоков. return;
    }
    Console.WriteLine("так");
    Monitor.Pulse(this);
    //
    Разрешает выполнение
    // метода tick().
    Monitor.Wait(this);
    //
    Ожидаем завершения
    // метода tick().
    }
    }
    } class MyThread { public Thread thrd;
    TickTock ttOb;
    //
    Создаем новый поток. public MyThread(string name, TickTock tt) { thrd = new Thread(new ThreadStart(this.run)); ttOb = tt; thrd.Name = name; thrd.Start();
    }
    //
    Начинаем выполнение нового потока. void run() { if(thrd.Name == "тик") { for(int i=0; i<5; i++) ttOb.tick(true); ttOb.tick(false);
    } else { for(int i=0; i<5; i++) ttOb.tock(true); ttOb.tock(false);
    }
    }
    } class TickingClock { public static void Main() {
    TickTock tt = new TickTock();
    MyThread mt1 = new MyThread("тик", tt);
    MyThread mt2 = new MyThread("так", tt); mt1.thrd.Join(); mt2.thrd.Join();
    Console.WriteLine("Часы остановлены");
    }
    }
    При выполнении эта программа сгенерировала следующие результаты:

    Глава 21. Многопоточное программирование
    597 тик так тик так тик так тик так тик так Часы остановлены
    Рассмотрим эту программу в деталях. В методе
    Main()
    создается объект класса
    TickTock с именем tt
    , который затем используется для запуска двух потоков. В методе run()
    класса
    MyThread выполняется сравнение имени текущего потока со словом “тик”.
    Если сравниваемые значения совпали, вызывается метод tick()
    , в противном случае — метод tock()
    . При этом для каждого из пяти вызовов каждого метода в качестве аргумента передается значение true
    . Часы “тикают” до тех пор, пока передается именно значение true
    . Последний вызов каждого метода (с передачей в качестве аргумента значения false
    ) останавливает часы.
    Самая важная часть программы сосредоточена в методах tick()
    и tock()
    Рассмотрим сначала метод tick()
    , который для удобства приведем ниже: public void tick(bool running) { lock(this)
    { if(!running) { // Останов часов.
    Monitor.Pulse(this);
    //
    Уведомление любых
    // ожидающих потоков. return;
    }
    Console.Write("тик ");
    Monitor.Pulse(this);
    //
    Разрешает выполнение
    // метода tock().
    Monitor.Wait(this);
    //
    Ожидаем завершения
    // метода tock().
    }
    }
    Прежде всего обратите внимание на то, что код метода tick()
    заключен в рамки lock
    -блока. Вспомните, методы
    Wait()
    и
    Pulse()
    можно использовать только внутри синхронизируемых блоков. Метод начинается с проверки значения параметра running
    Этот параметр используется для отключения часов. Если он равен значению false
    , часы будут остановлены. В этом случае вызывается метод
    Pulse()
    , который позволяет заработать ожидающему потоку. Сюда мы скоро вернемся, а пока предположим, что часы никто (пока) не останавливает, и на экране отображается слово “тик”, а затем происходит обращение к методу
    Pulse()
    с последующим вызовом метода
    Wait()
    . Обращение к методу
    Pulse()
    позволяет перейти в ожидание возможности доступа к тому же самому объекту. Вызов метода
    Wait()
    вынуждает метод tick()
    приостановиться до тех пор, пока другой поток не вызовет метод
    Pulse()
    . Таким образом, после вызова метода tick()
    отображается одно слово “тик”, выдается разрешение на “оживление” другого потока, после чего первый приостанавливается.
    Метод tock()
    — точная копия метода tick()
    , за исключением того, что он отображает слово “так”. Итак, после его вызова отображается одно слово “так”, вызывается метод
    Pulse()
    , а затем — и метод
    Wait()
    . Если рассматривать эти методы в паре, то за вызовом метода tick()
    может следовать только вызов метода tock()
    , за которым, в свою очередь, может следовать только вызов метода tick()
    и т.д.. Таким образом, эти два метода взаимно синхронизированы.
    Для вызова метода
    Pulse()
    при останове часов есть все основания, поскольку он позволяет успешно завершить последнее обращение к методу
    Wait()
    . Не забывайте,

    598
    Часть II. Библиотека C# что методы tick()
    и tock()
    после отображения соответствующего сообщения вызывают метод
    Wait()
    . Следовательно, в момент останова часов один из этих методов находится в состоянии ожидания. А чтобы вывести его из этого состояния, необходимо вызвать метод
    Pulse()
    в последний раз. Ради эксперимента попробуйте удалить этот вызов метода
    Pulse()
    и посмотрите, как это скажется на результатах выполнения программы. Нетрудно убедиться, что программа в этом случае “зависнет” и для выхода из нее вам придется нажать клавиши . Дело в том, что для завершающего вызова метода
    Wait()
    из последнего выполнения метода tock()
    нет соответствующего обращения к методу
    Pulse()
    , который бы позволил бы методу tock()
    благополучно завершиться. Поэтому он (метод tock()
    ) находится в бесконечном ожидании.
    Если у вас есть какие-либо сомнения насчет необходимости вызовов методов
    Wait()
    и
    Pulse()
    для правильной работы наших “часов”, то прежде чем переходить к следующему разделу, замените класс
    TickTock в предыдущей программе приведенной ниже версией. Здесь из класса удалены обращения к методам
    Wait()
    и
    Pulse()
    // Нерабочая версия класса TickTock. class TickTock { public void tick(bool running) { lock(this)
    { if(!running)
    {
    //
    Останов часов. return;
    }
    Console.Write("тик ");
    }
    } public void tock(bool running) { lock(this)
    { if(!running)
    {
    //
    Останов часов. return;
    }
    Console.WriteLine("так");
    }
    }
    }
    После замены класса
    TickTock результаты выполнения программы “часов” выглядят так: тик тик тик тик тик так так так так так
    Часы остановлены
    В этом случае очевидно, что методы tick()
    и tock()
    больше не синхронизированы!
    Взаимоблокировка
    При разработке многопоточных программ необходимо позаботиться о том, чтобы во время их выполнения не создалась тупиковая ситуация, вызванная взаимоблокировкой. При
    взаимоблокировке (deadlock) один поток ожидает, пока другой не выполнит

    Глава 21. Многопоточное программирование
    599 некоторое действие, но в то же время второй поток ожидает действия первого. Таким образом, оба потока приостановлены, ожидая друг друга, и ни один из них не выполняется.
    Эта ситуация напоминает двух чрезмерно вежливых людей, каждый из которых настаивает на том, чтобы другой прошел в дверь первым!
    Казалось бы, избежать взаимоблокировки нетрудно, но это не совсем так. Например, взаимоблокировка может возникнуть в ответвлениях программы. Рассмотрим класс
    TickTock
    . Как разъяснялось выше, если метод
    Pulse()
    не выполнится в последний раз из метода tick()
    или tock()
    , то другой метод (т.е. tock()
    или tick()
    ) будет находиться в состоянии бесконечного ожидания, и программа зависнет. Зачастую причину взаимоблокировки нелегко понять, просто изучая исходный код программы, поскольку одновременно выполняющиеся потоки могут сложным образом взаимодействовать во время работы. Чтобы избежать взаимоблокировки, необходимо очень аккуратно подходить к программированию и тщательно тестировать написанный код. Как правило, если многопоточная программа вдруг “виснет”, то наиболее вероятная причина этого — взаимоблокировка.
    Использование атрибута
    MethodImplAttribute
    Используя атрибут
    MethodImplAttribute
    , можно синхронизировать метод целиком. Этот подход можно рассматривать как альтернативу инструкции lock в случаях, когда необходимо заблокировать все содержимое метода.
    Атрибут
    MethodImplAttribute определен в пространстве имен
    System.Runtime.CompilerServices
    Конструктор, используемый для синхронизации, имеет такой вид: public MethodImplAttribute(MethodImplOptions
    opt
    )
    Здесь параметр
    opt
    задает атрибут реализации. Для синхронизации метода задайте атрибут
    MethodImplOptions.Synchronized
    . Этот атрибут обеспечивает блокировку всего метода.
    Рассмотрим версию класса
    TickTock
    , в которой для синхронизации используется атрибут
    MethodImplAttribute
    :
    // Использование атрибута MethodImplAttribute
    // для синхронизации метода. using System; using System.Threading; using System.Runtime.CompilerServices; class TickTock {
    /*
    Следующий атрибут синхронизирует метод tick() целиком. */
    [MethodImplAttribute(MethodImplOptions.Synchronized)] public void tick(bool running) { if(!running) { // Останов часов.
    Monitor.Pulse(this);
    //
    Уведомление для
    // ожидающих потоков. return;
    }
    Console.Write("Тик ");
    Monitor.Pulse(this);
    //
    Разрешаем работать

    600
    Часть II. Библиотека C#
    // методу tock().
    Monitor.Wait(this);
    //
    Ожидаем, пока не завершится
    // метод tock().
    }
    /*
    Следующий атрибут синхронизирует метод tock() целиком. */
    [MethodImplAttribute(MethodImplOptions.Synchronized)] public void tock(bool running) { if(!running) { // Останов часов.
    Monitor.Pulse(this);
    //
    Уведомление для
    // ожидающих потоков. return;
    }
    Console.WriteLine("Так");
    Monitor.Pulse(this);
    //
    Разрешаем работать
    // методу tick().
    Monitor.Wait(this);
    //
    Ожидаем, пока не завершится
    // метод tick().
    }
    } class MyThread { public Thread thrd;
    TickTock ttOb;
    //
    Создаем новый поток. public MyThread(string name, TickTock tt) { thrd = new Thread(new ThreadStart(this.run)); ttOb = tt; thrd.Name = name; thrd.Start();
    }
    //
    Начинаем выполнять новый поток. void run() { if(thrd.Name == "Тик") { for(int i=0; i<5; i++) ttOb.tick(true); ttOb.tick(false);
    } else { for(int i=0; i<5; i++) ttOb.tock(true); ttOb.tock(false);
    }
    }
    } class TickingClock { public static void Main() {
    TickTock tt = new TickTock();
    MyThread mt1 = new MyThread("Тик", tt);
    MyThread mt2 = new MyThread("Так", tt);

    Глава 21. Многопоточное программирование
    601 mt1.thrd.Join(); mt2.thrd.Join();
    Console.WriteLine("Часы остановлены.");
    }
    }
    Результаты выполнения программы с этим вариантом класса
    TickTock совпадают с приведенным выше (имеются в виду правильно работающие “часы”).
    При блокировании всего метода выбор между lock
    -инструкцией или атрибутом
    MethodImplAttribute за вами. Оба средства дают одинаковые результаты. Но поскольку lock — ключевое слово языка C#, то в примерах этой книги предпочтение отдано именно встроенному средству синхронизации.
    Приостановка, возобновление и завершение
    выполнения потоков
    Иногда выполнение потока необходимо приостановить. Например, поток можно использовать для отображения времени суток. Если пользователь желает убрать часы с экрана, соответствующий поток можно приостановить. Позже, когда необходимость в часах появится снова, выполнение приостановленного потока можно возобновить. В любом случае приостановить и возобновить поток — дело нехитрое. Иногда нужно и вовсе завершить выполнение потока. Завершение выполнения потока отличается от приостановки тем, что завершенный поток удаляется из системы, и его выполнение не может быть возобновлено в дальнейшем.
    Для приостановки потока используйте метод
    Thread.Suspend()
    , а для его возобновления — метод
    Thread.Resume()
    . Форматы использования этих методов таковы: public void Suspend() public void Resume()
    Если вызывающий поток находится не в подходящем для вызываемого метода состоянии, генерируется исключение типа
    ThreadStateException
    . Такие последствия может иметь, например, попытка возобновить поток, который не был приостановлен.
    Чтобы завершить поток, используйте метод
    Thread.Abort()
    . Самый простой формат его использования выглядит так: public void Abort()
    Метод
    Abort()
    генерирует исключение типа
    ThreadAbortException для потока, из которого этот метод вызван. Это исключение и заставляет поток завершиться.
    Кроме того, то же самое исключение может быть перехвачено программным кодом (с автоматической его регенерацией для завершения потока). Однако следует учитывать, что метод
    Abort()
    не всегда способен немедленно остановить выполнение потока, поэтому, если важно, чтобы поток был завершен до продолжения вашей программы, необходимо сопроводить вызов метода
    Abort()
    вызовом метода
    Join()
    . В некоторых (довольно редких) случаях метод
    Abort()
    не в состоянии завершить поток. Это возможно в ситуации, когда finally
    -блок включен в бесконечный цикл.
    В следующем примере демонстрируется приостановка, возобновление и завершение выполнения потока:
    //Приостановка, возобновление и завершение потока. using System; using System.Threading;

    602
    Часть II. Библиотека C# class MyThread { public Thread thrd; public MyThread(string name) { thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; thrd.Start();
    }
    //
    Это входная точка для потока. void run() {
    Console.WriteLine(thrd.Name + " стартовал."); for(int i = 1; i <= 1000; i++) {
    Console.Write(i + " "); if((i%10)==0)
    {
    Console.WriteLine();
    Thread.Sleep(250);
    }
    }
    Console.WriteLine(thrd.Name + " завершен.");
    }
    } class SuspendResumeStop { public static void Main() {
    MyThread mt1 = new MyThread("Мой поток");
    Thread.Sleep(1000);
    //
    Разрешаем стартовать
    // дочернему потоку. mt1.thrd.Suspend();
    Console.WriteLine("Приостановка выполнения потока.");
    Thread.Sleep(1000); mt1.thrd.Resume();
    Console.WriteLine("Возобновление выполнения потока.");
    Thread.Sleep(1000); mt1.thrd.Suspend();
    Console.WriteLine("Приостановка выполнения потока.");
    Thread.Sleep(1000); mt1.thrd.Resume();
    Console.WriteLine("Возобновление выполнения потока.");
    Thread.Sleep(1000);
    Console.WriteLine("Завершение выполнения потока."); mt1.thrd.Abort(); mt1.thrd.Join();
    //
    Ожидаем завершения выполнения потока.
    Console.WriteLine("Основной поток завершен.");
    }
    }

    Глава 21. Многопоточное программирование
    603
    Результаты выполнения этой программы таковы:
    Мой поток стартовал.
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
    Приостановка выполнения потока.
    41 42 43 44 45 46 47 48 49 50
    Возобновление выполнения потока.
    51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
    Приостановка выполнения потока.
    81 82 83 84 85 86 87 88 89 90
    Возобновление выполнения потока.
    91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
    Завершение выполнения потока.
    Основной поток завершен.
    Альтернативный формат использования метода Abort()
    В некоторых случаях удобней использовать второй формат метода
    Abort()
    : public void Abort(object
    info
    )
    Здесь параметр info содержит информацию, которую необходимо передать в поток при его завершении. Эта информация доступна через свойство
    ExceptionState класса
    ThreadAbortException
    . Этот формат можно использовать для передачи потоку кода завершения, что и демонстрируется в следующем примере:
    // Использование метода Abort(object). using System; using System.Threading; class MyThread { public Thread thrd; public MyThread(string name) { thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; thrd.Start();
    }
    //
    Это входная точка для потока. void run() { try
    {
    Console.WriteLine(thrd.Name
    +
    " стартовал."); for(int i = 1; i <= 1000; i++) {
    Console.Write(i + " "); if((i%10)==0)
    {
    Console.WriteLine();
    Thread.Sleep(250);
    }
    }
    Console.WriteLine(thrd.Name
    +
    " завершился нормально.");

    604
    Часть II. Библиотека C#
    } catch(ThreadAbortException exc) {
    Console.WriteLine(
    "Выполнение потока прервано, код завершения = " + exc.ExceptionState);
    }
    }
    } class UseAltAbort { public static void Main() {
    MyThread mt1 = new MyThread("Мой поток");
    Thread.Sleep(1000);
    //
    Разрешаем стартовать
    // дочернему потоку.
    Console.WriteLine("Прерывание выполнения потока."); mt1.thrd.Abort(100); mt1.thrd.Join();
    //
    Ожидаем завершения
    // выполнения потока.
    Console.WriteLine("Основной поток завершен.");
    }
    }
    Результаты выполнения этой таковы:
    Мой поток стартовал.
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
    Прерывание выполнения потока.
    41 42 43 44 45 46 47 48 49 50
    Выполнение потока прервано, код завершения = 100
    Основной поток завершен.
    Как подтверждают результаты выполнения этой программы, методу
    Abort()
    передано число 100. Это значение затем можно прочитать в свойстве
    ExceptionState класса исключения
    ThreadAbortException
    , перехватываемого потоком при завершении.
    Отмена действия метода Abort()
    Поток может переопределить запрос на прерывание выполнения. Для этого поток должен перехватить исключение типа
    ThreadAbortException
    , а затем вызвать метод
    ResetAbort()
    . Это защищает исключение от автоматической регенерации по окончании его обработки потоком. Метод
    ResetAbort()
    объявляется следующим образом: public static void ResetAbort()
    Обращение к методу
    ResetAbort()
    может оказаться неудачным, если поток не имеет соответствующего уровня доступа, чтобы отменить прерывание выполнения.
    Использование метода
    ResetAbort() демонстрируется в следующей программе:
    // Использование метода ResetAbort(). using System; using System.Threading; class MyThread { public Thread thrd;

    Глава 21. Многопоточное программирование
    605 public MyThread(string name) { thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; thrd.Start();
    }
    //
    Это входная точка для потока. void run() {
    Console.WriteLine(thrd.Name + " стартовал."); for(int i = 1; i <= 1000; i++) { try
    {
    Console.Write(i + " "); if((i%10)==0)
    {
    Console.WriteLine();
    Thread.Sleep(250);
    }
    } catch(ThreadAbortException exc) { if((int)exc.ExceptionState ==0) {
    Console.WriteLine(
    "Прерывание отменено! Код завершения = " + exc.ExceptionState);
    Thread.ResetAbort();
    } else
    Console.WriteLine(
    "Выполнение потока прервано, код завершения = " + exc.ExceptionState);
    }
    }
    Console.WriteLine(thrd.Name + " завершился нормально.");
    }
    } class ResetAbort { public static void Main() {
    MyThread mt1 = new MyThread("Мой поток");
    Thread.Sleep(1000);
    //
    Разрешаем стартовать
    // дочернему потоку.
    Console.WriteLine("Прерывание выполнения потока."); mt1.thrd.Abort(0);
    //
    Это не остановит выполнение
    // потока.
    Thread.Sleep(1000);
    //
    Разрешаем дочернему потоку
    // поработать немного дольше.
    Console.WriteLine("Прерывание выполнения потока."); mt1.thrd.Abort(100);
    //
    Эта инструкция в состоянии
    // остановить выполнение потока. mt1.thrd.Join();
    //
    Ожидаем завершения
    // выполнения потока.
    Console.WriteLine("Основной поток завершен.");
    }
    }

    606
    Часть II. Библиотека C#
    Результаты выполнения этой программы таковы:
    Мой поток стартовал.
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
    Прерывание выполнения потока.
    Прерывание отменено! Код завершения = 0 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
    Прерывание выполнения потока.
    Выполнение потока прервано, код завершения = 100
    Основной поток завершен.
    Вы убедились, что, если метод
    Abort()
    вызывается с аргументом, значение которого равно нулю, то посредством вызова метода
    ResetAbort()
    запрос на прерывание отменяется, и поток продолжает выполняться. Любое другое значение этого аргумента останавливает выполнение потока.
    Определение состояния потока
    Состояние потока можно получить из свойства
    ThreadState
    , определенного в классе
    Thread
    : public Thread ThreadState{ get; }
    Состояние потока возвращается как значение, определенное перечислением
    ThreadState
    . В нем определены такие значения:
    ThreadState.Aborted ThreadState.AbortRequested
    ThreadState.Background ThreadState.Running
    ThreadState.Stopped ThreadState.StopRequested
    ThreadState.Suspended ThreadState.SuspendRequested
    ThreadState.Unstarted ThreadState.WaitSleepJoin
    Одно из перечисленных выше значений требует пояснения. В состояние, представленное значением
    ThreadState
    WaitSleepJoin
    , поток входит, ожидая результатов вызова метода
    Wait()
    ,
    Sleep()
    или
    Join()
    Использование основного потока
    Как упоминалось в начале этой главы, все C#-программы имеют по крайней мере один поток управления, именуемый основным потоком, который автоматически создается в начале выполнения программы. Основной поток обрабатывается подобно всем остальным потокам.
    Чтобы получить доступ к основному потоку, необходимо иметь объект класса
    Thread
    , который его представляет. Это реализуется с помощью свойства
    CurrentThread
    , которое является членом класса
    Thread
    . Формат его использования таков: public static Thread CurrentThread{ get; }
    Это свойство возвращает ссылку на поток, в котором оно опрашивается.
    Следовательно, если при выполнении основного потока обратиться к свойству

    Глава 21. Многопоточное программирование
    607
    CurrentThread
    , мы получим ссылку на основной поток. Имея такую ссылку, можно управлять основным потоком, как и любым другим.
    В следующей программе демонстрируется, как получить ссылку на основной поток, узнать его имя и приоритет, а также задать новое имя и приоритет:
    // Управление основным потоком. using System; using System.Threading; class UseMain { public static void Main() {
    Thread thrd;
    //
    Получаем ссылку на объект основного потока. thrd = Thread.CurrentThread;
    //
    Отображаем имя основного потока. if(thrd.Name == null)
    Console.WriteLine("Основной поток не имеет имени."); else
    Console.WriteLine("Имя основного потока: " + thrd.Name);
    //
    Отображаем приоритет основного потока.
    Console.WriteLine("Приоритет: " + thrd.Priority);
    Console.WriteLine();
    //
    Задаем имя и приоритет.
    Console.WriteLine("Установка имени и приоритета.\n"); thrd.Name = "Основной поток"; thrd.Priority = ThreadPriority.AboveNormal;
    Console.WriteLine(
    "У основного потока теперь есть имя: " + thrd.Name);
    Console.WriteLine("Приоритет теперь таков: " + thrd.Priority);
    }
    }
    Результаты выполнения этой программы таковы:
    Основной поток не имеет имени. Приоритет: Normal
    Установка имени и приоритета.
    У основного потока теперь есть имя: Основной поток Приоритет теперь таков: AboveNormal
    Предупреждение: будьте осторожны при выполнении операций над основным потоком. Например, если обращение к методу
    Join() thrd.Join(); добавить в конец метода
    Main()
    , программа никогда не завершится, поскольку она будет ожидать, пока не завершится основной поток!

    608
    Часть II. Библиотека C#
    Совет по созданию многопоточных программ
    Ключ к эффективному использованию многопоточности лежит в “параллельном” мышлении (в противоположность последовательному). Например, если в вашей программе предполагается функционирование двух параллельно работающих подсистем, организуйте их в виде отдельных потоков. Но если создать слишком много потоков, реальное быстродействие программы может пострадать. Помните: каждое переключение контекста требует определенных расходов системных ресурсов. При большом количестве потоков на изменение контекста будет потрачено больше процессорного времени, чем на выполнение самой программы!
    Запуск отдельной задачи
    Несмотря на то что чаще всего в C# -программировании используется поточно- ориентированная многозадачность, в соответствующих случаях возможно применение и процессно-ориентированной многозадачности. В многозадачной среде, ориентированной на процессы, вместо запуска в той же программе еще одного потока, одна программа запускает на выполнение другую программу. В C# это реализуется с использованием класса Process, который определен в пространстве имен
    System.Diagnostics
    Самый простой способ запустить другую программу — использовать метод
    Start()
    , определенный в классе
    Process
    . Вот один из простейших его форматов: public static Process Start(string
    name
    )
    Здесь параметр
    name
    означает имя выполняемого файла или файла, связанного с выполняемым.
    При завершении созданного процесса вызовите метод
    Close()
    , чтобы освободить память, занимаемую этим процессом. Метод
    Close()
    объявляется так: public void Close()
    Завершить процесс можно двумя способами. Если процесс представляет собой GUI- приложение, ориентированное на выполнение под управлением Windows, то для завершения такого процесса достаточно вызвать метод
    CloseMainWindow()
    , определяемый следующим образом: public bool CloseMainWindow()
    Этот метод посылает процессу сообщение, предписывающее ему остановиться.
    Метод возвращает значение true
    , если посланное сообщение получено. Метод возвращает значение false
    , если данное приложение не является GUI-программой или не имеет главного окна. Более того, метод
    CloseMainWindow()
    — лишь запрос на прекращение работы. Если он игнорируется приложением, завершение не состоится.
    Для безусловного завершения процесса необходимо вызвать метод
    Kill()
    : public void Kill()
    Однако использовать метод
    Kill()
    нужно с большой осторожностью. Он обеспечивает бесконтрольное завершение процесса. Любые несохраненные данные, связанные с этим процессом, будут, скорее всего, утеряны.
    Ожидать завершения процесса можно с помощью метода
    WaitForExit()
    . Его два возможных формата таковы: public void WaitForExit() public bool WaitForExit(int
    milliseconds
    )

    Глава 21. Многопоточное программирование
    609
    Первый формат позволяет ожидать до тех пор, пока процесс не завершится, а второй
    — лишь в течение заданного числа миллисекунд. Метод
    WaitForExit()
    , используемый во втором формате, возвращает значение true
    , если в течение заданного промежутка времени процесс завершился, и значение false
    , если он все еще выполняется. Следующая программа демонстрирует создание процесса и ожидание его завершения. Программа запускает стандартную утилиту Windows
    WordPad.exe
    // Запуск нового процесса. using System; using System.Diagnostics; class StartProcess { public static void Main() {
    Process newProc = Process.Start("wordpad.exe");
    Console.WriteLine("Новый процесс стартовал."); newProc.WaitForExit(); newProc.Close();
    //
    Освобождаем ресурсы системы.
    Console.WriteLine("Новый процесс завершился.");
    }
    }
    При выполнении этой программы запускается текстовый редактор
    WordPad
    , и на экране появится сообщение “Новый процесс стартовал”. Затем программа будет ожидать, пока вы не закроете утилиту
    WordPad
    . После завершения работы текстового редактора
    WordPad отобразится последнее сообщение “Новый процесс завершился”.

    Полный справочник по
    1   ...   36   37   38   39   40   41   42   43   ...   52


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