|
"...Изучение и анализ примеров.
В и приведены описания и приложены исходные коды параллельных программ..." Непонятно что такое - "В и приведены описания" и где именно приведены и приложены исходные коды. |
Класс 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);