Асинхронное программирование. Часть 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<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. Пусть это будет факультативным заданием для тех кто смог дочитать до этого места

До новых встреч.