В прошлой статье посвященной асинхронной модели программирования мы рассмотрели шаблон базирующийся на событиях и я пообещал в следующей своей статье рассмотреть альтернативную модель которая использует 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<DirectoryDescriptor> 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<DirectoryDescriptor> 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<DirectoryDescriptor> 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<DirectoryDescriptor> _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<string, IList<DirectoryDescriptor>> 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<DirectoryDescriptor> End() { if(!IsCompleted) { _waitHandle.WaitOne(); _waitHandle.Close(); } if (_currentException != null) throw _currentException; return _result; }
Мы им воспользуемся в реализации EndEnumDirectories, суть происходящего сводится к … дождаться выполнения асинхронной операции если это нужно и вернуть результат выполнения кода если не возникло никаких осложнений.По сути мы выполнили условие пункта 4 при котором клиент не дождавшись выполнения асинхронной операции вызывает метод EndEnumDirectories (мы дождемся за него )). Последний штрих конструктор класса:
public DirectoryInfoAsyncResult(Func<string, IList<DirectoryDescriptor>> 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<DirectoryDescriptor> 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<DirectoryDescriptor> EndEnumDirectories(IAsyncResult asyncResult) { var result = (DirectoryInfoAsyncResult)asyncResult; return result.End(); } }
Осталось продемонстрировать все четыре способа получения результатов работы: 1)
var manager = new DirectoryInfoManager(); var ar = manager.BeginEnumDirectories("D:\\", null, null); // некий полезный код IList<DirectoryDescriptor> directories = manager.EndEnumDirectories(ar);
2)
var manager = new DirectoryInfoManager(); var ar = manager.BeginEnumDirectories("D:\\", null, null); // некий полезный код while (!ar.IsCompleted) { // некий полезный код }
IList<DirectoryDescriptor> directories = manager.EndEnumDirectories(ar); 3)
var manager = new DirectoryInfoManager(); var ar = manager.BeginEnumDirectories("D:\\", null, null); // некий полезный код if(!ar.AsyncWaitHandle.WaitOne(1000, false)) { // выводим прогресс работы } IList<DirectoryDescriptor> directories = manager.EndEnumDirectories(ar);
4)
var manager = new DirectoryInfoManager(); manager.BeginEnumDirectories("D:\\", result => { IList<DirectoryDescriptor> directories = manager.EndEnumDirectories(result); }, null); // некий полезный код
В заключение: Реализацию DirectoryInfoAsyncResult, очень легко сделать параметризованной на основе generic. Пусть это будет факультативным заданием для тех кто смог дочитать до этого места
До новых встреч.