Опубликован: 15.10.2009 | Доступ: свободный | Студентов: 887 / 247 | Оценка: 4.42 / 4.20 | Длительность: 08:22:00
Специальности: Программист
Лекция 6:

Класс System.Threading.Tasks.Future и координирующие структуры данных

< Лекция 5 || Лекция 6: 12 || Лекция 7 >

Семинарское занятие № 6. Асинхронная модель программирования и класс Future<T>

В данном занятии будет показано как

  1. можно реализовать "асинхронную модель программирования" (АМП) с использованием объектов класса Future<T>, и, наоборот, как
  2. можно создавать объекты класса Future<T> с использованием имеющейся реализации АМП.

Таким образом, будет показано как Task Parallel Library интегрирована с асинхронными механизмами, уже имеющимися в .NET Framework.

Базовыми конструкциями, лежащими в основе АМП, являются

  1. метод BeginXx, с помощью которого запускается асинхронная операция, и который возвращает объект типа IAsyncResult, и
  2. метод EndXx, который принимает в качестве входного аргумента IAsyncResult и возвращает вычисленное значение.

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

System.Threading.Tasks.Future<T>,

поскольку класс Future<T> наследует класс

System.Threading.Tasks.Task

и реализует интерфейс IAsyncResult (отметим, что интерфейс IAsyncResult является в .NET, в действительности, объектом типа System.Runtime.Remoting.Messaging.AsyncResult ).

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

public class PI
  {
    public string Calculate (int decimalPlaces )
    {
      .   .   .
    }
  }
Пример 6.1.

Реализовать АМП, в данном случае, означает ввести в класс PI методы BeginCalculate и EndCalculate, позволяющие использовать метод Calculate асинхронным образом. Используя Future<T>, это можно сделать, добавив лишь несколько строк кода:

public class PI
  {  . . .
    public IAsyncResult BeginCalculate (int decimalPlaces)
    {
     return Future.Create ( () => Calculate(decimalPlaces) );
    }
    public string EndCalculate ( IAsyncResult ar )
    {
     var f = ar as Future<string>;
     if ( f == null )
      throw new ArgumentException ("ar" );
     return f.Value;
    }
  }
Пример 6.2.

На самом деле, в классической асинхронной модели программирования требуется также, чтобы метод BeginXx мог принимать еще 2 аргумента:

  1. функцию "обратного вызова" ( AsyncCallback ), которая автоматически вызывается по завершении асинхронной операции, и
  2. объект, задающий состояние программы пользователя в момент асинхронного вызова ( user-defined state object ).

Добавление функции AsyncCallback очень легко реализовать, используя возможности, предоставляемые классом Future<T>:

public IAsyncResult BeginCalculate (int decimalPlaces, 
                                          AsyncCallback ac )
  {
   var f = Future.Create ( () => Calculate (decimalPlaces) );
   if ( ac != null )
    f.Completed += delegate { ac ( f ); };
   return f;
  }
Пример 6.3.

Решение, реализованное в пример 6.3, состоит в регистрации для объекта класса Future<T> события Completed, при наступлении которого теперь будет вызываться функция AsyncCallback.

Задача 1.

Реализуйте полностью класс PI с методами BeginCalculate и EndCalculate, добавив к нему соответствующий метод Main для проверки всего решения.

Однако, текущая реализация класса Future<T> не поддерживает, в отличие от класса Task, аргумент, задающий состояние. Тем не менее, эту трудность можно обойти, реализовав свой собственный класс на основе Future<T>, который будет принимать объект, задающий состояние:

private class FutureWithState<T> : IAsyncResult
  {
   public IAsyncResult Future;
   public object       State;

   public object AsyncState { get { return State; } }
   public WaitHandle AsyncWaitHandle {
    get { return Future.AsyncWaitHandle; } }
   public bool CompletedSynchronously {
    get { return Future.CompletedSynchronously; } }
   public bool IsCompleted {
    get { return Future.IsCompleted; } }
  }
Пример 6.4.

Модификации методов BeginCalculate и EndCalculate, поддерживающие задание состояния, очевидны:

public IAsyncResult BeginCalculate 
(int decimalPlaces, AsyncCallback ac, object state )
  {
   var f = Future.Create ( () => Calculate(decimalPlaces) );
   if ( ac != null ) f.Completed += delegate { ac ( f ); };
   return new FutureWithState<string> {
    Future = f, State = state };
  }
  public string EndCalculate ( IAsyncResult ar )
  {
   var f = ar as FutureWithState<string>;
   if ( f == null )
      throw new ArgumentException ("ar" );
   return f.Future.Value;
  }
Пример 6.5.

Аналогичный подход на основе создания контейнера может быть применен и для других сценариев, в которых требуется сохранение большего количества аргументов в добавок к IAsyncResult ( Future<T> ).

В качестве примера реализации АМП в конкретном случае, рассмотрим класс System.Net.NetworkInformation.Ping, который был введен в .NET Framework 2.0. Этот класс, на самом деле, выводится из класса Component и соответствует схеме асинхронного программирования, базирующегося на событиях ( Event-based Asynchronous Pattern - EAP ). А потому, в отличие от АМП, которая предоставляет методы BeginSend и EndSend, данный класс имеет метод SendAsync, который возвращает void, а также содержит событие PingCompleted, которое происходит по заверешении асинхронной операции посылки сигнала ping. С помощью Future<T> легко реализовать функциональность класса Ping в рамках АМП:

public class MyPing
{
    public IAsyncResult BeginPing(string host)
    {
        return Future.Create(() => new Ping().Send(host));
    } 

    public PingReply EndPing(IAsyncResult ar)
    {
        var f = ar as Future<PingReply>;
        if (f == null) throw new ArgumentException("ar");
        return f.Value;
    }
}
Пример 6.6.

Задача 2.

Реализуйте метод Main для данного класса, в котором асинхронным образом пингуется несколько хостов, имена которых заданы в виде списка или массива.

Однако, реализация, приведенная в пример 6.6, обладает одним недостатком - функция Send, содержащая мало вычислительных операций, является синхронной операцией, а потому будет блокировать поток, в рамках которого она будет выполняться. Чтобы обеспечить реальную асинхронность операции BeginPing, нужно воспользоваться асинхронной операцией SendAsync класса Ping, а результат операции пингования получить через свойство Value объекта класса Future<T>:

public class MyPing
{
    public IAsyncResult BeginPing(string host, AsyncCallback ac)
    {
        var f = Future<PingReply>.Create();
        if (ac != null) f.Completed += delegate { ac(f); };

        Ping p = new Ping();
        p.PingCompleted += (sender, state) =>
        {
            if (state.Error != null) f.Exception = state.Error;
            else f.Value = state.Reply;
        };
        p.SendAsync(host, null);

        return f;
    } 

    public PingReply EndPing(IAsyncResult ar)
    {
        var f = ar as Future<PingReply>;
        if (f == null) throw new ArgumentException("ar");
        return f.Value;
    }
}
Пример 6.7.

В пример 6.7, объект класса Future<T> создается без делегата, связанного с ним:

var f = Future<PingReply>.Create();

Поток, обратившийся за значением Value объекта Future<T>, заблокируется до тех пор, пока не будут установлены свойства Value или Exception этого объекта, которые, в свою очередь, получат значение в обработчике события PingCompleted. Таким образом, мы здесь имеем полностью асинхронную реализацию механизма Ping, соответствующую асинхронной модели программирования (АМП).

Задача 3.

Рассмотрите пример 6.3 и выясните может ли произойти такая ситуация, когда к моменту регистрации события Completed, выполнение функции, связанной с Future, уже закончится, а потому не будет произведен необходимый вызов функции AsyncCallback 1[Подсказка: найдите в документации CTP Parallel Extensions правила регистрации делегатов для события Completed ].

Теперь решим обратную задачу - покажем, как используя имеющуюся реализацию АМП, создавать объекты класса Future<T>. Суть подхода состоит в том, что объект Future<T> может создаваться без задания делегата Func<T>, представляющего вычислительную функцию, которая должна выполняться в рамках Future<T>. Свойства же Value и Exception объекта Future<T>, в этом случае, устанавливаются явно при завершении асинхронной операции:

static Future<T> Create<T> (
  Action<AsyncCallback> beginFunc, 
  Func<IAsyncResult, T> endFunc)
{
    var f = Future<T>.Create();
    beginFunc(iar => {
        try { f.Value = endFunc(iar); }
        catch (Exception e) { f.Exception = e; }
    });
    return f;
}
Пример 6.8.

В пример 6.8, вначале создается объект класса Future<T> как таковой. Затем происходит вызов делегата beginFunc, запускающий исполнение асинхронной операции. В качестве аргумента этого вызова передается делегат функции, которая будет вызываться по завершении асинхронной операции.

Покажем, как описанный подход работает в случае его применения к (асинхронным) операциям чтения из файла. В частности, класс FileStream имеет следующие асинхронные операции BeginRead и EndRead:

IAsyncResult BeginRead(
        byte[] array, int offset, int numBytes, 
        AsyncCallback userCallback, object stateObject);
int EndRead(IAsyncResult asyncResult);
Пример 6.9.

Таким образом, если мы хотим создать объект Future<T>, который представляет асинхронную операцию чтения для FileStream, достаточно применить вновь определенный метод Create ( пример 6.8) следующим образом:

var readFuture = Create<int> (
        ac => fs.BeginRead(buffer, 0, buffer.Length, ac, null),
        fs.EndRead);
Пример 6.10.

Задача 4.

Написать метод Main, в котором попеременно асинхронно читаются 2 файла с использованием объектов Future<T>, аналогичных показанному в пример 6.10.

Таким образом, если объект класса FileStream был создан с поддержкой асинхронных операций ввода/вывода и версия ОС Windows поддерживает асинхронный ввод/вывод, то при запросе на чтение из файла, будет создан объект Future<T>, но дополнительного потока создано не будет.

Способы реализации метода Create объекта Future<T>, показанного в пример 6.8, могут различаться. Например, альтернативная версия может базироваться на использовании ThreadPool:

static Future<T> Create<T> (
        IAsyncResult iar, Func<IAsyncResult, T> endFunc)
{
    var f = Future<T>.Create();
    ThreadPool.RegisterWaitForSingleObject(
            iar.AsyncWaitHandle, delegate {
        try { f.Value = endFunc(iar); }
        catch (Exception e) { f.Exception = e; }
    }, null, -1, true);
    return f;
}
Пример 6.11.

В пример 6.11, вместо делегата beginFunc, используется обращение к методу RegisterWaitForSingleObject класса ThreadPool. Когда произойдет событие AsyncWaitHandle, которое принадлежит классу IAsyncResult, и причиной которого является завершение асинхронной операции, то произойдет вызов endFunc и будут установлены свойства Value или Exception объекта Future<T> как в предыдущем случае. Данная версия метода Create может быть использована следующим образом:

var readFuture = Create<int> (
        fs.BeginRead(buffer, 0, buffer.Length, null, null), 
        fs.EndRead);
Пример 6.12.

Задача 5.

Реализуйте метод Main для использования функции Create из пример 6.11, и распечатайте ID главного потока и потока в рамках которого будет выполняться присваивание

f.Value = endFunc(iar);
< Лекция 5 || Лекция 6: 12 || Лекция 7 >
Максим Полищук
Максим Полищук
"...Изучение и анализ примеров.
В и приведены описания и приложены исходные коды параллельных программ..."
Непонятно что такое - "В и приведены описания" и где именно приведены и приложены исходные коды.