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

68

Наиболее простым способом для создания потока является определение делегата и его вызов асинхронным образом. Делегаты могут исполнять роль безопасных для типов ссылок на методы. Помимо этого класс Delegate поддерживает и возможность асинхронного вызова этих методов. Для решения поставленной задачи он создает "за кулисами" отдельный поток.

На объявление делегата .NET компилятор C# отвечает построением запечатанного класса, который наследуется от System.MulticastDelegate (который, в свою очередь, унаследован от System.Delegate). Эти базовые классы предоставляют каждому делегату возможность поддерживать список адресов методов, которые могут быть вызваны позднее.

// Тип делегата C#.
public delegate int BinaryOp(int x, int y);

Исходя из определения, BinaryOp может указывать на любой метод, принимающий два целых числа (по значению) в качестве аргументов и возвращающий целое число. После компиляции сборка с определением делегата будет содержать полноценное определение класса, сгенерированного динамически при построении проекта, на основе объявления делегата. В случае BinaryOp этот класс более или менее похож на следующий (записан в псевдокоде):

public sealed class BinaryOp : System.MulticastDelegate
{
   public BinaryOp(object target, uint functionAddress);
   public void Invoke(int x, int y);
   public IAsyncResult Beginlnvoke(int x, int y, AsyncCallback cb, object state);
   public int Endlnvoke(IAsyncResult result);
}

Сгенерированный метод Invoke() используется для вызова метода, поддерживаемого объектом делегата в синхронном режиме. Поэтому вызывающий поток (такой как первичный поток приложения) должен будет ждать, пока не завершится вызов делегата. Также вспомните, что в C# метод Invoke() не нужно вызывать в коде напрямую — он может быть инициирован неявно, "за кулисами", при применении "нормального" синтаксиса вызова метода.

Теперь можно применять различные приемы для асинхронного вызова данного делегата и возврата результатов.

Одним из таких приемов является опрос и проверка, завершил ли делегат свою работу. Созданный класс delegate предоставляет метод BeginInvoke(), в котором могут передаваться входные параметры, определенные вместе с типом делегата. Метод BeginInvoke() всегда имеет два дополнительных параметра типа AsyncCallback и object, которые будут рассматриваться позже. Сейчас главный интерес представляет возвращаемый тип BeginInvoke() — IAsyncResult. С помощью IAsyncResult можно извлекать информацию о делегате и проверять, завершил ли он свою работу, что здесь и делается с применением свойства IsCompleted. Цикл while продолжает выполняться в главном потоке программы до тех пор, пока делегат не завершит работу:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleApplication1
{
    public delegate int BinaryOp(int data, int time);

    class Program
    {
        static void Main()
        {
            Console.WriteLine("Синхронный вызов метода: \n");
            // Синхронный вызов метода
            DelegateThread(1,5000);

            Console.WriteLine("\nАсинхронный вызов метода с двумя потоками: \n");
            // Асинхронный вызов метода с применением делегата
            BinaryOp bo = DelegateThread;

            IAsyncResult ar = bo.BeginInvoke(1, 5000, null, null);
            while (!ar.IsCompleted)
            {
                // Выполнение операций в главном потоке
                Console.Write(".");
                Thread.Sleep(50);
            }
            int result = bo.EndInvoke(ar);
            Console.WriteLine("Result: " + result);

            Console.ReadLine();
        }

        static int DelegateThread(int data, int time)
        {
            Console.WriteLine("DelegateThread запущен");
            // Делаем задержку, для эмуляции длительной операции
            Thread.Sleep(time);
            Console.WriteLine("DelegateThread завершен");
            return ++data;
        }
    }
}

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

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

Вместо выполнения проверки на предмет того, завершил ли делегат работу, после завершения работы главным потоком можно вызвать метод EndInvoke() типа делегата. Метод EndInvoke() сам ожидает, когда делегат завершит свою работу. Если главный поток завершает выполнение, не дожидаясь завершения работы делегата, поток делегата останавливается.

Метод BeginInvoke() всегда возвращает объект, реализующий интерфейс IAsyncResult, в то время как EndInvoke() ожидает единственный параметр совместимого с IAsyncResult типа. Совместимый с IAsyncResult объект, возвращаемый из BeginInvoke() — это в основном связывающий механизм, который позволяет вызывающему потоку получить позже результат вызова асинхронного метода через EndInvoke().

Интерфейс IAsyncResult (находящийся в пространстве имен System) определен следующим образом:

public interface IAsyncResult
{
   object AsyncState { get; }
   WaitHandle AsyncWaitHandle { get; }
   bool CompletedSynchronously { get; }
   bool IsCompleted { get; }
}

Другой способ ожидания результата от асинхронного делегата предусматривает применение дескриптора ожидания (wait handle), который ассоциируется с IAsyncResult. Получить доступ к этому дескриптору ожидания можно через свойство AsyncWaitHandle. Это свойство возвращает объект типа WaitHandle, с помощью которого можно организовать ожидание завершения работы потоком делегата.

Метод WaitOne() принимает в качестве первого необязательного параметра значение тайм-аута, в котором можно указать максимальный период времени ожидания. В рассматриваемом здесь примере этот период составляет 50 миллисекунд. В случае возникновения тайм-аута метод WaitOne() возвращает значение false, и проход по циклу while продолжается. Если во время ожидания до тайм-аута дело не доходит, осуществляется выход из цикла while с помощью break и получение результата методом EndInvoke(). В пользовательском интерфейсе результат выглядит примерно так же, как в предыдущем примере, отличается лишь способ реализации ожидания.

Пройди тесты
Лучший чат для C# программистов