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

ОГЛАВЛЕНИЕ

Статья объясняет делегаты и способы их асинхронного использования.

Введение

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

Вызывающий поток не блокируется; он может выполнять какую-то иную задачу. Инфраструктура .NET получает поток для вызова метода и доставляет входной параметр, переданный вызывающим кодом. Затем асинхронный поток может выполнять метод параллельно вызывающему потоку. Если метод генерирует некие данные и возвращает это значение, вызывающий поток должен иметь доступ к этим данным. Асинхронное свойство .NET поддерживает два механизма: вызывающий поток может запросить результаты, или инфраструктура может доставить результаты вызывающему потоку, когда результаты готовы. Цель данной статьи – объяснить делегаты и способы их асинхронного использования.

Делегаты

В C# новый тип делегата создается с помощью ключевого слова delegate:

public delegate void MyDelegate(int x, int y);

Был создан новый тип делегата по имени MyDelegate, созданный поверх методов с типами возвращаемой переменной void(пустой) и принимающий два аргумента типа int. Затем делегат может быть сформирован поверх цели, передан и вызван когда-нибудь позже. Вызов в C# выглядит как обычный вызов функции:

class Foo
{
   void PrintPair(int a, int b)
   {
     Console.WriteLine("a = {0}", a);
     Console.WriteLine("b = {0}", b);
   }

   void CreateAndInvoke()
   {
     // подразумевается 'new MyDelegate(this.PrintPair)':
     MyDelegate del = PrintPair;
     del(10, 20);
  }
}

Еще не понимаете? CreateAndInvoke создает новый MyDelegate, сформированный поверх метода PrintPair с текущим указателем this в качестве цели. Реальный промежуточный язык, порожденный компилятором C#, показывает ряд сложностей делегатов в нижележащей системе типов:

struct MyDelegate :  System.MulticastDelegate
{
  public MyDelegate(object target, IntPtr, methodPtr);

  private object target;
  private IntPtr methodPtr;

  public internal void Invoke(int x, int y)
  public internal System.IAsyncResult BeginInvoke(int x, int y,
                  System.IAsyncCallback callback, object state);
  public internal void EndInvoke(System.IAsyncResult  result);

}

Конструктор используется для формирования делегата поверх целевого объекта и указателя функции. Методы Invoke, BeginInvoke и EndInvoke реализуют операцию вызова делегата и помечены как внутренние (т.е. времени выполнения на промежуточном языке), чтобы указать CLR(общеязыковая среда исполнения), что она предоставляет реализацию; их тела IL(промежуточный язык) оставляются пустыми. Invoke выполняет синхронный вызов, тогда как функции BeginInvoke и EndInvoke следуют схеме модели асинхронного программирования. Тип MyDelegate нарушает правило в том, что структуры не могут наследоваться от других типов, отличных от System.ValueType. Делегаты имеют специальную поддержку в обобщённой системе типизации (CTS), поэтому это допустимо. MyDelegate унаследован от MulticastDelegate; этот тип является общей базой для всех делегатов, созданных в C#, и поддерживает делегаты, имеющие несколько целей.

Рассмотрим создание еще одного делегата:

public delegate int StringDelegate(string str);

Его можно объявить в классе или в глобальной области видимости. Компилятор C# сгенерирует новый класс на основе этого объявления и унаследует его от System.MulticastDelegate. Рассмотрим методы данного класса и его базового класса System.Delegate (снова):

public sealed class StringDelegate : System.MulticastDelegate
{
   public StringDelegate (object obj, int method);
   public virtual int Invoke(string str);
   public virtual IAsyncResult BeginInvoke(string str,
      AsyncCallback asc, object stateObject);
   public virtual int EndInvoke(IAsyncResult result);
}
Теперь рассмотрим код, относящийся к созданию первого делегата MyDelegate:
using System;
public static class App {
   public delegate void MyDelegate(int x, int y);
   public static void PrintPair(int a, int b)
   {
     Console.WriteLine("a = {0}", a);
     Console.WriteLine("b = {0}", b);
   }
   public static void Main()
   {
     // предполагается 'new MyDelegate(this.PrintPair)':
     MyDelegate d = PrintPair;
     // предполагается 'Invoke':
     d(10, 20);
   }
}

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

a = 10
b = 20

Внутренности делегатов

Был объявлен тип MyDelegate на C# так:

delegate string MyDelegate(int x);

Он представляет собой тип указателя функции, который может ссылаться на любой метод, принимающий единственный целый аргумент и возвращающий строку. При работе с экземпляром данного типа делегата объявляются переменные типа MyDelegate. Тайно компилятор генерирует новый класс (тип):

private sealed class MyDelegate : MulticastDelegate
{
    public extern MyDelegate(object object, IntPtr method);
    public extern virtual string Invoke(int x);
    public extern virtual IAsyncResult BeginInvoke(int x,
                  AsyncCallback  callback, object   object);
    public extern virtual string Endinvoke((IAsyncResult result);
}

Допустим, что есть пользовательский тип MyType с методом MyFunc, сигнатура которого точно совпадает с MyDelegate. Заметьте, что параметры не названы идентично. Это нормально, так как делегаты лишь требуют, чтобы ожидаемые типы находились в нужных позициях сигнатуры:

class MyType
{
  public string MyFunc(int foo)
  {
     return "MyFunc called with the value  '" + foo + "' foo foo;
  }
}

Имея тип делегата в метаданных и вызываемую целевую функцию, формируем экземпляр делегата поверх цели. Это создает новый экземпляр типа делегата с помощью конструктора MyDelegate(object, IntPtr). Код передает цель в качестве первого аргумента и указатель на функцию кода в виде второго аргумента. Синтаксис для этого таков:

MyType mt = new MyType();
MyDelegate md = mt.MyFunc;
Объединим все части вместе:
using System;
delegate string MyDelegate(int x);
class MyType
{
  public string MyFunc(int foo)
  {
     return "MyFunc called with the value '" + foo + "' for foo";
  }
}

public class Program {
  public static void Main()
  {
    MyType mt = new MyType();
    MyDelegate md = mt.MyFunc;
    Console.WriteLine(md.Invoke(5));
    Console.WriteLine(md(5));
  }
}

Код компилируется и выдает такой результат:

MyFunc вызвана со значением '5' для foo
MyFunc вызвана со значением '5' для foo

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

Перед использованием асинхронного делегата следует помнить, что все типы делегатов автоматически предоставляют два метода по имени 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.