=== Асинхронное программирование. Часть 2. APM (Asynchronous Programming Model) ===
В прошлой статье посвященной асинхронной модели программирования мы рассмотрели шаблон базирующийся на событиях и я пообещал в следующей своей статье рассмотреть альтернативную модель которая использует APM (Asynchronous Programming Model) паттерн. Как было сказано в предисловии к первой статье данный подход нацелен на потребителей которым является библиотечный код. Мы не будем пытаться больше синхронизировать наш код с потоком в котором выполняется GUI приложения, а в замен получим большую гибкость и несколько возможных сценариев получения результатов выполнения кода. Начнем. Для тестирования нашего кода используем следующую программную модель.
Класс DirectoryDescriptor
public class DirectoryDescriptor
{
public string Name { get; set; }
public string Path { get; set; }
}
Класс описывает физический каталог (имя, путь).
Класс DirectoryManager инкапсулирующий логику по работе с директориями. Один из его методов позволяет получить список каталогов по заданному пути. Ниже приводится тестовая реализация:
public class DirectoryInfoManager
{
public IList EnumDirectories(string path)
{
return
(from d in Directory.GetDirectories(path)
select new DirectoryDescriptor {Name = Path.GetFileName(d), Path = d}).ToList();
}
}
Итак мы имеем синхронное API. Наша цель применив APM паттерн добиться того, что бы наш код по получению информации о директориях мог выполняться асинхронно.
Согласно утвердившийся практике, APM API должен предоставить два метода с префиксами Begin и End. Первый позволяет вызывать метод асинхронно, второй позволяет получить результат его выполнения и обработать возможные сообщения об ошибках, читай исключения . Стандартная реализация APM требует обеспечения четырех возможных способов получения результата работы асинхронного кода.
1) Посредством callback метода передаваемого в метод Begin
2) Посредством использования WaitHandle для получения уведомления о том, что код выполнен.
3) Проверкой флага состояния о выполнении кода
4) Прямым вызовом метода с префиксом End, в этом случае нам нужно обеспечить внутреннюю модель ожидания выполнения кода и получения результатов его выполнения.
Прежде, чем двинуться дальше мы могли бы подумать о том, что бы мы могли написать для предоставления таких возможностей клиенту. С callback методом все просто мы могли бы его принять параметром в метод Begin и вызвать когда наш код выполниться, при этом мы могли бы написать некий вспомогательный код, который позволял бы передавать и сам метод для выполнения. В нашем случае это была бы синхронная реализация. В конечном итоге это чем то напоминает событийную модель за тем лишь исключением, что результат работы будет получен клиентом не из аргументов события, а внутри callback метода путем вызова метода End. Для того, что бы выполнить пункт 2 нам потребуется вернуть из метода Begin клиенту ссылку на ManualResetEvent, клиент сможет ожидать уведомление об окончании выполнения метода Begin и получив его сможет воспользоваться результатом его работы вызвав соответствующий метод End. Аналогично пункт 3 предполагает возврат флага, проверяя который клиент получит возможность дождаться окончания выполнения Begin метода. Отсюда легко сделать вывод, что нам потребуется некий класс являющий собой агрегат для WaitHandle и управляющего флага. Имея WaitHandle несложно добиться синхронизации работы при прямом вызове метода End. Посредством такого класса было бы удобно и передать результаты работы метода Begin.
Для поддержки всех четырех возможных сценариев .NET Framework предлагает нам самостоятельно реализовать IAsyncResult интерфейс. С его помощью клиент нашего класса, сможет синхронизировать свой код и получит возможность извлекать результат работы метода BeginXXX.
Давайте взглянем на этот интерфейс:
public interface IAsyncResult
{
// Properties
object AsyncState { get; }
WaitHandle AsyncWaitHandle { get; }
bool CompletedSynchronously { get; }
bool IsCompleted { get; }
}
Что же мы видим? По порядку AsyncState некий объект состояния по которому клиент сможет отличать один асинхронный вызов от другого. В большинстве ситуации равен null.
AsyncWaitHandle - обычно экземпляр класса ManualResetEvent, который позволит сообщить о событии завершения кода из одного потока в другой.
CompletedSynchronously – отвечает за уведомление клиента о том, что код выполнится синхронно, в нашем случае это не актуально и мы будем всегда возвращать false.
IsCompleted – булева переменная проверяя которую клиент может узнать о том, что пришло время узнать о результатах выполнения асинхронного кода.
По сути очень похоже на обобщение поведения для всех наших 4-x способов получить результат выполнения. Реализовав такой интерфейс и вернув экземпляр такого класса из метода Begin, мы сможем обеспечить выполнение пунктов 2, 3, 4 для клиента нашего класса, а добавив callback параметром в метод Begin, мы выполним и пункт под номером 1. Общий вид метода BeginXXX будет следующим:
IAsyncResult BeginXXX(те же параметры, что и для синхронного метода, AsyncCallback callback, object state);
Для метода EndXXX будет
справедлив следующий шаблон:
ТипВозвратКакВсинхронойВерсии EndXXX(IAsyncResult result);
Применив такие шаблоны к нашему менеджеру мы получим следующий код:
public IList EnumDirectories(string path)
{
return
(from d in Directory.GetDirectories(path)
select new DirectoryDescriptor {Name = Path.GetFileName(d), Path = d}).ToList();
}
public IAsyncResult BeginEnumDirectories(string path, AsyncCallback callback, object state)
{
}
public IList EndEnumDirectories(IAsyncResult asyncResult)
{
}
Что бы наполнить методы реализацией нам потребуется реализовать интерфейс IAsyncResult для этого мы создадим класс DirectoryInfoAsyncResult
Объявим следующие переменные:
private volatile int _isComplited;
private readonly ManualResetEvent _waitHandle;
private readonly AsyncCallback _callback;
private readonly object _state;
private Exception _currentException;
private IList _result;
Остановлюсь на некоторых моментах, которые мне кажутся важными. Прежде всего я решил использовать вместо булевой переменной IsCompleted переменную типа int переменная будет изменяться и мониториться из разных потоков, а потому я страхуясь использую ключевое слово volatile. Стоит заметить, что для CLR это не принципиально и можно использовать переменную типа bool, а вот код под Mono потребует этого в обязательном порядке.
Сам интерфейс реализуется тривиальнейшим способом:
public bool IsCompleted { get { return _isComplited == 1; }}
public WaitHandle AsyncWaitHandle { get { return _waitHandle; }}
public object AsyncState { get{ return _state;} }
public bool CompletedSynchronously { get { return false; } }
Все, что нам осталось сделать это добавить парочку вспомогательных методов:
Первый метод будет иметь возможность запускать код асинхронно:
private void RunAsynchronously(Func> func, string path)
{
ThreadPool.QueueUserWorkItem(o =>
{
try
{
_result = func(path);
}
catch (Exception ex)
{
_currentException = ex;
}
finally
{
_isComplited = 1;
_waitHandle.Set();
if (_callback != null)
_callback(this);
}
});
}
Метод принимает метод синхронного вызова и выполняет его асинхронно, через пул потоков. В результате либо сохраняет результат или же сохраняет возникший Exception в соответствующую переменную. В любом из двух сценариев мы обеспечиваем возможность получения уведомления о завершении метода: устанавливаем флаг, устанавливаем в сигнальное положение handle, вызываем callback метод.
Теперь вспомогательный метод End:
public IList End()
{
if(!IsCompleted)
{
_waitHandle.WaitOne();
_waitHandle.Close();
}
if (_currentException != null)
throw _currentException;
return _result;
}
Мы им воспользуемся в реализации EndEnumDirectories, суть происходящего сводится к … дождаться выполнения асинхронной операции если это нужно и вернуть результат выполнения кода если не возникло никаких осложнений.По сути мы выполнили условие пункта 4 при котором клиент не дождавшись выполнения асинхронной операции вызывает метод EndEnumDirectories (мы дождемся за него )).
Последний штрих конструктор класса:
public DirectoryInfoAsyncResult(Func> func, string path, AsyncCallback callback, object state)
{
_callback = callback;
_state = state;
_waitHandle = new ManualResetEvent(false);
RunAsynchronously(func, path);
}
Мы передаем синхронный метод, аргументы для его выполнения, callback метода и state если это необходимо. Метод сразу же выполняем, а callback и state сохраняем в соответствующие переменные.
Воспользовавшись вышеприведённым кодом мы можем полностью реализовать наш DirectoryInfoManager:
public class DirectoryInfoManager
{
public IList EnumDirectories(string path)
{
return
(from d in Directory.GetDirectories(path)
select new DirectoryDescriptor {Name = Path.GetFileName(d), Path = d}).ToList();
}
public IAsyncResult BeginEnumDirectories(string path, AsyncCallback callback, object state)
{
return new DirectoryInfoAsyncResult(EnumDirectories, path, callback, state);
}
public IList EndEnumDirectories(IAsyncResult asyncResult)
{
var result = (DirectoryInfoAsyncResult)asyncResult;
return result.End();
}
}
Осталось продемонстрировать все четыре способа получения результатов работы:
1)
var manager = new DirectoryInfoManager();
var ar = manager.BeginEnumDirectories("D:\\", null, null);
// некий полезный код
IList directories = manager.EndEnumDirectories(ar);
2)
var manager = new DirectoryInfoManager();
var ar = manager.BeginEnumDirectories("D:\\", null, null);
// некий полезный код
while (!ar.IsCompleted)
{
// некий полезный код
}
IList directories = manager.EndEnumDirectories(ar);
3)
var manager = new DirectoryInfoManager();
var ar = manager.BeginEnumDirectories("D:\\", null, null);
// некий полезный код
if(!ar.AsyncWaitHandle.WaitOne(1000, false))
{
// выводим прогресс работы
}
IList directories = manager.EndEnumDirectories(ar);
4)
var manager = new DirectoryInfoManager();
manager.BeginEnumDirectories("D:\\", result =>
{
IList directories = manager.EndEnumDirectories(result);
}, null);
// некий полезный код
В заключение: Реализацию DirectoryInfoAsyncResult, очень легко сделать параметризованной на основе generic. Пусть это будет факультативным заданием для тех кто смог дочитать до этого места
До новых встреч.