Делегаты каркаса .NET: Основные сведения об асинхронных делегатах - Асинхронные делегаты?

ОГЛАВЛЕНИЕ

Так что такое асинхронные делегаты?

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

delegate int MyDelegate(int x, int y)

предоставляет следующие методы согласно генерации компилятора:

IAsyncResult BeginInvoke(int x, int y, AsyncCallback callback, 
             object object, IAsyncResult result);
int EndInvoke (IAsyncResult result);

Эти два метода генерируются компилятором. Чтобы вызвать метод асинхронно, сначала надо сослаться на него с помощью объекта делегата, имеющего такую же сигнатуру. Затем надо вызвать BeginInvoke для этого объекта делегата. Компилятор следит, чтобы первыми аргументами метода BeginInvoke были аргументы вызываемого метода. Последние два аргумента этого метода, IAsyncResult и object, будут рассмотрены кратко. Возвращаемое значение асинхронного вызова извлекается путем вызова метода EndInvoke. Тут компилятор тоже следит, чтобы возвращаемое значение EndInvoke было точно таким же, как и возвращаемое значение типа делегата (в данном примере это тип int). Вызов EndInvoke блокирующий, то есть вызов вернет управление только при завершении асинхронного выполнения. Следующий пример показывает асинхронный вызов метода ShowSum:

using System;
using System.Threading;
public class Program {
 public delegate int TheDelegate( int x, int y);
 static int ShowSum( int x, int y ) {
 int sum = x + y;
 Console.WriteLine("Thread #{0}: ShowSum()  Sum = {1}",
                   Thread.CurrentThread.ManagedThreadId, sum);
 return sum;
}
public static void Main() {
  TheDelegate d = ShowSum;
  IAsyncResult ar = d.BeginInvoke(10, 10, null, null);
  int sum = d.EndInvoke(ar);
  Console.WriteLine("Thread #{0}: Main()     Sum = {1}",
                    Thread.CurrentThread.ManagedThreadId, sum);
  }
}

Вывод:

Thread #3: ShowSum()  Sum = 20
Thread #1: Main()     Sum = 20

Метод BeginInvoke имеет параметр для каждого параметра нижележащего делегата (как Invoke) и добавляет два параметра: делегат IAsyncCallback, вызываемый при завершении асинхронного выполнения, и object, передаваемый как значение свойства IAsyncResult.AsyncState функции обратного вызова. Метод возвращает IAsyncResult, который можно использовать для отслеживания выполнения, ожидания WaitHandle или завершения асинхронного вызова.

public interface IAsyncResult
{
    // Свойства
    object AsyncState { get; }
    WaitHandle AsyncWaitHandle { get; }
    bool CompletedSynchronously { get; }
    bool IsCompleted { get; }
}

Когда делегат завершит выполнение, надо вызвать EndInvoke для делегата, передав IAsyncResult. EndInvoke очищает WaitHandle (если он был выделен), генерирует исключение, если делегат не выполнился правильно, и имеет тип возвращаемой переменной, совпадающий с типом у нижележащего метода. Он возвращает значение, возвращенное вызовом делегата:

using System;
public sealed class Program {
  delegate int IntIntDelegate(int x);
  private static int Square(int x) { return x * x; }
  private static void AsyncDelegateCallback(IAsyncResult ar)
  {
    IntIntDelegate f = (IntIntDelegate)ar.AsyncState;
    Console.WriteLine(f.EndInvoke(ar));
  }

  public static void Main()
  {
    IntIntDelegate f = Square;

    /* Вариант 1: Ожидание в состоянии занятости (быстрый метод делегата) */
    IAsyncResult ar1 = f.BeginInvoke(10, null, null);
    while (!ar1.IsCompleted)
        // Делаем какую-то затратную работу, пока он выполняется.
    Console.WriteLine(f.EndInvoke(ar1));

    /* Вариант 2: Ожидание WaitHandle wait (более долгий метод делегата) */
    IAsyncResult ar2 = f.BeginInvoke(20, null, null);
    // Делаем какую-то работу.
    ar2.AsyncWaitHandle.WaitOne();
    Console.WriteLine(f.EndInvoke(ar2));

    /* Вариант 3: Подход обратного вызова */
    IAsyncResult ar3 = f.BeginInvoke(30, AsyncDelegateCallback, f);
    // Возвращаемся из метода (пока делегат выполняется).
  }
}

Вывод:

100
400

Теперь, если метод имеет параметры, являющиеся ссылочными типами и передаваемые как параметры, то асинхронный метод сможет вызвать методы для ссылочных типов, меняющие его состояние. Это можно использовать для возврата значений из асинхронного метода. Код ниже показывает пример этого. Делегат GetData принимает объект типа System.Array в качестве входного параметра. Так как параметр передается по ссылке, данный метод может менять значения в массиве. Однако так как к объекту могут обращаться два потока, надо убедиться, что вы не обращаетесь к совместно используемому объекту, пока асинхронный метод не завершится:

using System;
class App
{
   delegate void GetData(byte[] b);
   static void GetBuf(byte[] b)
   {
      for (byte x = 0; x < b.Length; x++)
         b[x] = (byte)(x*x);
   }
   static void Main()
   {
      GetData d = new GetData(App.GetBuf);
      byte[] b = new byte[10];
      IAsyncResult ar;
      ar = d.BeginInvoke(b, null, null);
      ar.AsyncWaitHandle.WaitOne();
      for (int x = 0; x < b.Length; x++)
         Console.Write("{0} ", b[x]);
   }
}

Вывод:

0
1
4
9
16
25
36
49
64
81
100

Чтобы использовать метод обратного вызова, надо сослаться на него с помощью объекта делегата типа System.AsyncCallback, переданного после последнего аргумента методу BeginInvoke. Этот метод должен приспособиться к типу делегата, а значит, он должен иметь тип возвращаемой переменной void (в случае примера ниже) и принимать единственный аргумент типа IAsyncResult:

using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;
class Program {
  public delegate int MyDelegate(int x, int y);
  static AutoResetEvent e = new AutoResetEvent(false);
  static int WriteSum( int x, int y) {
  Console.WriteLine("Thread# {0}: Sum = {1}",
          Thread.CurrentThread.ManagedThreadId, x + y);
  return x + y;
}

static void SumDone(IAsyncResult async) {
  Thread.Sleep( 1000 );
  // AsyncResult пространства имен System.Runtime.Remoting.Messaging
  MyDelegate func = ((AsyncResult) async).AsyncDelegate as MyDelegate;
  int sum = func.EndInvoke(async);
  Console.WriteLine("Thread# {0}: Callback method sum = {1}",
                    Thread.CurrentThread.ManagedThreadId, sum);
  e.Set();
}

static void Main() {
  MyDelegate func = WriteSum;

  // компилятор C# 2.0 логически выводит объект делегата типа
  // AsyncCallback, чтобы сослаться на метод SumDone()
  IAsyncResult async = func.BeginInvoke(10, 10, SumDone, null);
  Console.WriteLine("Thread# {0}: BeginInvoke() called! Wait for SumDone() completion.",
                    Thread.CurrentThread.ManagedThreadId);
  e.WaitOne();
  Console.WriteLine("Thread# {0}: Bye....",
                    Thread.CurrentThread.ManagedThreadId);
  }
}

Компиляция данного кода дает следующий результат:

Thread# 1: BeginInvoke() called! Wait for SumDone() completion.
Thread# 3: Sum = 20
Thread# 3: Callback method sum = 20
Thread# 1: Bye....

Важное замечание о control.BeginInvoke в сравнении с delegate.BeginInvoke

Данная статья использовала синтаксис C# 2.0, и поэтому в ее основе лежало написание кода делегата на платформе каркаса .NET 2.0. Сообщаем это, потому что часто в технической документации тема делегатов объединяется или связывается с событиями. Событие является сообщением, отправляемым объектом (управляющим элементом), чтобы сообщить об осуществлении действия. Действие бывает вызвано каким-то взаимодействием с пользователем, например, нажатием кнопки мыши, или инициировано какой-то иной логикой программы. Объект, возбуждающий событие, называется отправителем события. Объект, улавливающий событие и реагирующий на него, называется получателем события. При передаче события класс отправителя события не знает, какой объект или метод получит (обработает) возбужденное им событие. Нужен посредник (или механизм, подобный указателям) между источником и получателем.

Специальный тип под названием «делегат» обеспечивает функциональность указателя функции. Делегат – класс, хранящий ссылку на метод. Как было сказано, класс делегата имеет сигнатуру и может хранить ссылки только на методы, соответствующие его сигнатуре. При написании кода для форм Windows следующий пример показывает объявление делегата события:

public delegate void AlarmEventHandler( object sender, EventArgs e);

Одновременно с изучением способов асинхронного использования делегатов надо разъяснить любое иное использование делегатов (в данном случае, интерфейс пользователя форм Windows). Стандартная сигнатура делегата обработчика события определяет метод, не возвращающий значение, первый параметр которого имеет тип «Объект» и ссылается на экземпляр, возбуждающий событие, а второй параметр которого унаследован от типа EventArgs и хранит данные об этом событии. EventHandler является предопределенным делегатом, представляющим метод обработчика события для события, не генерирующего данные. Чтобы соединить событие с методом, обрабатывающим событие, экземпляр делегата добавляется в событие. Обработчик события вызывается всегда, когда наступает событие, если делегат не удален. Теперь кратко рассмотрим многопоточность и приложения с GUI(графический интерфейс пользователя).

Если вы занимались разработкой под Win32, то знаете, что обычно к API(интерфейс программирования приложений) обращаются синхронно; поток инициирует некую задачу, затем терпеливо ждет завершения выполнения задачи. Если код достигает более продвинутого уровня, он создает рабочий поток для осуществления этого синхронного вызова, приостанавливая основной поток, чтобы продолжить свою работу. Использование рабочих потоков для выполнения длительных блокирующих вызовов крайне важно для приложений GUI, потому что блокирующий поток, накачивающий очередь сообщений, отключает UI приложения. Процесс создания рабочих потоков труден. Создание потока накладно.

Создание нового рабочего потока каждый раз, когда надо сделать блокирующий вызов, может дать больше потоков, чем надо, повышая потребление ресурсов. В .NET асинхронное выполнение является ценным методом разработки. Например, вы должны использовать асинхронное выполнение в приложении форм Windows®, когда вам надо выполнить долго работающую команду без блокирования отзывчивости интерфейса пользователя. Программирование с делегатами упрощает асинхронное выполнение команды во вспомогательном потоке. Стало быть, можно создать приложение форм Windows, выполняющее длительные вызовы через сеть без заморозки интерфейса пользователя. В этом заключается проблема control.BeginInvoke в сравнении с delegate.BeginInvoke. При вызове Control.BeginInvoke вызов Control.Invoke осуществляется в потоке пула потоков, и вызов BeginInvoke возвращает управление сразу. По сути, рабочий поток вызывает BeginInvoke. Затем поток пула потоков принимает на себя управление и вызывает Control.Invoke, ожидая возвращаемое значение. Делегат, переданный в Invoke, затем вызывается в потоке интерфейса пользователя. Если вы вызываете Delegate.BeginInvoke, то переходите из рабочего потока в поток пула потоков, где выполняется метод, на который указывает делегат. Если вы обращаетесь к элементам интерфейса пользователя в этом потоке, то можете получить непредсказуемые результаты. Чтобы вызвать Control.BeginInvoke с конечной целью получить совпадение, EndInvoke позволяет продолжать обработку в вызвавшем его потоке, а не ждать обновления интерфейса пользователя. Анализ различий между BeginInvoke и Control.BeginInvoke делегата показывает, что BeginInvoke() управляющего элемента следит за тем, чтобы делегат вызывался в потоке, создавшем контекст управляющего элемента. Отсюда следует, что никакие потоки пула потоков не задействуются. Это было продемонстрировано тестовым приложением .NET MVP.