"...Изучение и анализ примеров.
В и приведены описания и приложены исходные коды параллельных программ..." Непонятно что такое - "В и приведены описания" и где именно приведены и приложены исходные коды. |
Класс System.Threading.Tasks.Future и координирующие структуры данных
Семинарское занятие № 6. Асинхронная модель программирования и класс Future<T>
В данном занятии будет показано как
- можно реализовать "асинхронную модель программирования" (АМП) с использованием объектов класса Future<T>, и, наоборот, как
- можно создавать объекты класса Future<T> с использованием имеющейся реализации АМП.
Таким образом, будет показано как Task Parallel Library интегрирована с асинхронными механизмами, уже имеющимися в .NET Framework.
Базовыми конструкциями, лежащими в основе АМП, являются
- метод BeginXx, с помощью которого запускается асинхронная операция, и который возвращает объект типа IAsyncResult, и
- метод 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 аргумента:
- функцию "обратного вызова" ( AsyncCallback ), которая автоматически вызывается по завершении асинхронной операции, и
- объект, задающий состояние программы пользователя в момент асинхронного вызова ( 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);