Опубликован: 15.09.2010 | Доступ: свободный | Студентов: 4806 / 630 | Оценка: 3.97 / 3.80 | Длительность: 14:45:00
Лекция 10:

Делегаты и события

< Лекция 9 || Лекция 10: 1234

Многопоточные приложения

Приложение .NET состоит из одного или нескольких процессов. Процессу принадлежат выделенная для него область оперативной памяти и ресурсы. Каждый процесс может состоять из нескольких доменов (частей) приложения, ресурсы которых изолированы друг от друга. В рамках домена может быть запущено несколько потоков выполнения. Поток (thread) представляет собой часть исполняемого кода программы. Иногда термин "thread" переводится буквально — "нить", чтобы отличить его от потоков ввода-вывода. Поэтому в литературе можно встретить и термин "многонитевые приложения".

В каждом процессе есть первичный поток, исполняющий роль точки входа в приложение. Для консольных приложений это метод Main.

Многопоточные приложения создают как для многопроцессорных, так и для однопроцессорных систем. Основной целью при этом являются повышение общей производительности и сокращение времени реакции приложения. Управление потоками осуществляет операционная система. Каждый поток получает некоторое количество квантов времени, по истечении которого управление передается другому потоку. Это создает у пользователя однопроцессорной машины впечатление одновременной работы нескольких потоков и позволяет, к примеру, выполнять ввод текста одновременно с длительной операцией по передаче данных.

Недостатки многопоточности:

  • большое количество потоков ведет к увеличению накладных расходов, связанных с переключением потоков, что снижает общую производительность;
  • возникают проблемы синхронизации данных, связанные с потенциальной возможностью доступа к одним и тем же данным со стороны нескольких потоков (например, если один поток начинает изменение общих данных, а отведенное ему время истекает, доступ к этим же данным может получить другой поток, который, изменяя данные, необратимо их повреждает).

Класс Thread

В .NET многопоточность поддерживается в основном с помощью пространства имен System.Threading. Некоторые типы этого пространства описаны в таблице 10.1.

Таблица 10.1. Некоторые типы пространства имен System.Threading
Тип Описание
Monitor Класс, обеспечивающий синхронизацию доступа к объектам
Mutex Класс-примитив синхронизации, который используется также для синхронизации между процессами
Thread Класс, который создает поток, устанавливает его приоритет, получает информацию о состоянии
ThreadPool Класс, используемый для управления набором взаимосвязанных потоков — пулом потоков
Timer Класс, определяющий механизм вызова заданного метода в заданные интервалы времени для пула потоков
WaitHandle Класс, инкапсулирующий объекты синхронизации, которые ожидают доступа к разделяемым ресурсам
ThreadStart Делегат, представляющий метод, который должен быть выполнен при запуске потока
TimerCallback Делегат, представляющий метод, обрабатывающий вызовы от класса Timer
WaitCallback Делегат, представляющий метод для элементов класса ThreadPool
ThreadPriority Перечисление, описывающее приоритет потока
ThreadState Перечисление, описывающее состояние потока

Первичный поток создается автоматически. Для запуска вторичных потоков используется класс Thread. При создании объекта-потока ему передается делегат, определяющий метод, выполнение которого выделяется в отдельный поток:

Thread t = new Thread ( new ThreadStart( имя_метода ) );

После создания потока заданный метод начинает в нем свою работу, а первичный поток продолжает выполняться. В листинге 10.6 приведен пример одновременной работы двух потоков.

using System;
using System.Threading;
namespace ConsoleApplication1
{    class Program
    {
        static public void Hedgehog()         // метод для вторичного потока
        {
            for ( int i = 0; i < 6; ++i ) 
            {
                Console.Write(" " + i ); Thread.Sleep( 1000 );
            }
        }
    
        static void Main()
        {
            Console.WriteLine( "Первичный поток " + 
                Thread.CurrentThread.GetHashCode() );

            Thread ta = new Thread( new ThreadStart(Hedgehog) );
            Console.WriteLine( "Вторичный поток " + ta.GetHashCode() );
            ta.Start();

            for ( int i = 0; i > -6; --i ) 
            {
                Console.Write( " " + i ); Thread.Sleep( 400 );
            }
        }
    }
}
Листинг 10.6. Создание вторичного потока

Результат работы программы:

Первичный поток 1
Вторичный поток 2
 0 0 -1 -2 1 -3 -4 2 -5 3 4 5

В листинге используется метод Sleep, останавливающий функционирование потока на заданное количество миллисекунд. Как видите, оба потока работают одновременно. Если бы они работали с одним и тем же файлом, он был бы испорчен так же, как и приведенный вывод на консоль, поэтому такой способ распараллеливания вычислений имеет смысл только для работы с различными ресурсами.

В таблице 10.2 перечислены основные элементы класса Thread.

Таблица 10.2. Основные элементы класса Thread
Элемент Вид Описание
CurrentThread Статическое свойство Возвращает ссылку на выполняющийся поток (только для чтения)
Name Свойство Установка текстового имени потока
Priority Свойство Получить/установить приоритет потока (используются значения перечисления ThreadPriority )
ThreadState Свойство Возвращает состояние потока (используются значения перечисления ThreadState )
Abort Метод Генерирует исключение ThreadAbortException. Вызов этого метода обычно завершает работу потока
Sleep Статический метод Приостанавливает выполнение текущего потока на заданное количество миллисекунд
Interrupt Метод Прерывает работу текущего потока
Join Метод Блокирует вызывающий поток до завершения другого потока или указанного промежутка времени и завершает поток
Resume Метод Возобновляет работу после приостановки потока
Start Метод Начинает выполнение потока, определенного делегатом ThreadStart
Suspend Метод Приостанавливает выполнение потока. Если выполнение потока уже приостановлено, то игнорируется

Можно создать несколько потоков, которые будут совместно использовать один и тот же код. Пример приведен в листинге 10.7.

using System;
using System.Threading;
namespace ConsoleApplication1
{
    class Class1
    {   public void Do()
        {
            for ( int i = 0; i < 4; ++i ) 
                { Console.Write( " " + i ); Thread.Sleep( 3 ); }
        }
    }
    
    class Program
    {   static void Main()
        {
            Class1 a = new Class1();
            Thread t1 = new Thread( new ThreadStart( a.Do ) );
            t1.Name = "Second";
            Console.WriteLine( "Поток " + t1.Name );
            t1.Start();

            Thread t2 = new Thread( new ThreadStart( a.Do ) );
            t2.Name = "Third";
            Console.WriteLine( "Поток " + t2.Name );
            t2.Start();
        }
    }
}
Листинг 10.7. Потоки, использующие один объект

Результат работы программы:

Поток Second
Поток Third
 0 0 1 1 2 2 3 3

Варианты вывода могут несколько различаться, поскольку один поток прерывает выполнение другого в неизвестные моменты времени.

Для того чтобы блок кода мог использоваться в каждый момент только одним потоком, применяется оператор lock. Формат оператора:

lock ( выражение ) блок_операторов

Выражение определяет объект, который требуется заблокировать. Для обычных методов в качестве выражения используется ключевое слово this, для статических — typeof( класс ). Блок операторов задает критическую секцию кода, которую требуется заблокировать.

Например, блокировка операторов в приведенном ранее методе Do выглядит следующим образом:

public void Do()
{
    lock( this )
    {
        for ( int i = 0; i < 4; ++i ) 
            { Console.Write( " " + i ); Thread.Sleep( 30 ); }
    }
}

Для такого варианта метода результат работы программы изменится:

Поток Second
Поток Third
 0 1 2 3 0 1 2 3

Асинхронные делегаты

Делегат можно вызвать на выполнение либо синхронно, как во всех приведенных ранее примерах, либо асинхронно с помощью методов BeginInvoke и EndInvoke.

При вызове делегата с помощью метода BeginInvoke среда выполнения создает для исполнения метода отдельный поток и возвращает управление оператору, следующему за вызовом. При этом в исходном потоке можно продолжать вычисления.

Если при вызове BeginInvoke был указан метод обратного вызова, этот метод вызывается после завершения потока. Метод обратного вызова также задается с помощью делегата, при этом используется стандартный делегат AsyncCallback. В методе обратного вызова для получения возвращаемого значения и выходных параметров применяется метод EndInvoke.

Если метод обратного вызова не был указан в параметрах метода BeginInvoke, метод EndInvoke можно использовать в потоке, инициировавшем запрос.

В листинге 10.8 приводится два примера асинхронного вызова метода, выполняющего разложение числа на множители. Листинг приводится по документации Visual Studio с некоторыми изменениями.

Класс Factorizer содержит метод Factorize, выполняющий разложение на множители. Этот метод асинхронно вызывается двумя способами: в методе Num1 метод обратного вызова задается в BeginInvoke, в методе Num2 имеют место ожидание завершения потока и непосредственный вызов EndInvoke.

using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;
                                                      // асинхронный делегат
public delegate bool AsyncDelegate ( int Num, out int m1, out int m2 );

                         // класс, выполняющий разложение числа на множители
public class Factorizer
{
    public bool Factorize( int Num, out int m1, out int m2 )
    {
        m1 = 1;    m2 = Num;
        for ( int i = 2; i < Num; i++ )
            if ( 0 == (Num % i) ) { m1 = i; m2 = Num / i; break; }

        if (1 == m1 ) return false;
        else          return true;
    }
}

                                  // класс, получающий делегат и результаты
public class PNum
{
    private int Number;
    public PNum( int number ) { Number = number; }

    [OneWayAttribute()]
                                             // метод, получающий результаты
    public void Res( IAsyncResult ar )
    {
        int m1, m2; 
                                        // получение делегата из AsyncResult
        AsyncDelegate ad = (AsyncDelegate)((AsyncResult)ar).AsyncDelegate;

                        // получение результатов выполнения метода Factorize
        ad.EndInvoke( out m1, out m2, ar );
                                                        // вывод результатов
        Console.WriteLine( "Первый способ : множители {0} : {1} {2}", 
                           Number, m1, m2 );
    }
}
                                                   // демонстрационный класс
public class Simple
{
                          // способ 1: используется функция обратного вызова
    public void Num1()
    {
        Factorizer     f = new Factorizer();
        AsyncDelegate ad = new AsyncDelegate ( f.Factorize );

        int Num = 1000589023, tmp; 
                         // создание экземпляра класса, который будет вызван 
                                 // после завершения работы метода Factorize
        PNum n = new PNum( Num );

                                 // задание делегата метода обратного вызова
        AsyncCallback callback = new AsyncCallback( n.Res );

                                       // асинхронный вызов метода Factorize
        IAsyncResult ar = ad.BeginInvoke(
                             Num, out tmp, out tmp, callback, null ); 
        //
        // здесь - выполнение неких дальнейших действий
        // ...
    }
                     // способ 2: используется ожидание окончания выполнения
    public void Num2()
    {
        Factorizer     f = new Factorizer();
        AsyncDelegate ad = new AsyncDelegate ( f.Factorize );

        int Num = 1000589023, tmp; 

                         // создание экземпляра класса, который будет вызван 
                                 // после завершения работы метода Factorize
        PNum n = new PNum( Num );

                                 // задание делегата метода обратного вызова
        AsyncCallback callback = new AsyncCallback( n.Res );

                                       // асинхронный вызов метода Factorize
        IAsyncResult ar = ad.BeginInvoke(
                             Num, out tmp, out tmp, null, null ); 
                                                      // ожидание завершения
        ar.AsyncWaitHandle.WaitOne( 10000, false );

        if ( ar.IsCompleted )
        {
            int m1, m2; 
                        // получение результатов выполнения метода Factorize
            ad.EndInvoke( out m1, out m2, ar );
                                                        // вывод результатов
            Console.WriteLine( "Второй способ : множители {0} : {1} {2}", 
                              Num, m1, m2 );
        }
    }

    public static void Main()
    {
        Simple s = new Simple();
        s.Num1();
        s.Num2();
    }
}
Листинг 10.8. Асинхронные делегаты

Результат работы программы:

Первый способ : множители 1000589023 : 7 142941289
Второй способ : множители 1000589023 : 7 142941289

Примечание

Атрибут [OneWayAttribute()] помечает метод как не имеющий возвращаемого значения и выходных параметров.

На этом данный курс лекций завершается. В него не вошли многие темы, рассмотренные в учебнике [4]: структуры, перечисления, работа с файлами, регулярные выражения, сборки, атрибуты, небезопасный код, основы программирования под Windows. Это связано с формулировкой договора, заключенного автором с издательством ПИТЕР при публикации книги.

Вопросы и задания для самостоятельной работы студента

  1. Опишите синтаксис делегата. Как добавить метод в делегат?
  2. Расскажите о способах использования делегатов.
  3. Какие операции можно выполнять с делегатами?
  4. Опишите реализацию событий в C#.
  5. Опишите паттерн "наблюдатель".
  6. Какой стандартный класс используется для передачи информации о событии?
  7. Изучите разделы стандарта C#, касающиеся делегатов.
  8. Изучите разделы стандарта C#, касающиеся событий.
< Лекция 9 || Лекция 10: 1234
Рахматулло Турсунов
Рахматулло Турсунов
Таджикистан, Душанбе