Версия для печати
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум на Исходниках.RU > .NET FAQ > Асинхронное программирование


Автор: juice 03.01.10, 14:26
В прошлой статье посвященной асинхронной модели программирования мы рассмотрели шаблон базирующийся на событиях и я пообещал в следующей своей статье рассмотреть альтернативную модель которая использует APM (Asynchronous Programming Model) паттерн. Как было сказано в предисловии к первой статье данный подход нацелен на потребителей которым является библиотечный код. Мы не будем пытаться больше синхронизировать наш код с потоком в котором выполняется GUI приложения, а в замен получим большую гибкость и несколько возможных сценариев получения результатов выполнения кода. Начнем. Для тестирования нашего кода используем следующую программную модель.
Класс DirectoryDescriptor

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
        public class DirectoryDescriptor
        {
            public string Name { get; set; }
            public string Path { get; set; }
        }


Класс описывает физический каталог (имя, путь).
Класс DirectoryManager инкапсулирующий логику по работе с директориями. Один из его методов позволяет получить список каталогов по заданному пути. Ниже приводится тестовая реализация:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
        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.
Давайте взгляним на этот интерфейс:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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 будет следующим:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    IAsyncResult BeginXXX(те же парамметры, что и для синхронного метода, AsyncCallback callback, object state);
Для метода EndXXX будет

справедлив следующий шаблон:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    ТипВозвратКакВсинхронойВерсии EndXXX(IAsyncResult result);


Применив такие шаблоны к нашему менеджеру мы получим следующий код:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
            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
Объявим следующие переменные:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
            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 переменная будет изменяться и мониторится из разных потоков, а потому я страхуясь использую ключевое слово volotile. Стоит заметить, что для CLR это не принципиально и можно использовать переменную типа bool, а вот код под Mono потребует этого в обязательном порядке.
Сам интерфейс реализуется тривиальнейшим способом:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
            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; } }


Все, что нам осталось сделать это добавить парочку вспомогательных методов:
Первый метод будет иметь возможность запускать код асинхронно:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
           public IList<DirectoryDescriptor> End()
            {
                if(!IsCompleted)
                {
                    _waitHandle.WaitOne();
                    _waitHandle.Close();
                }
     
                if (_currentException != null)
                    throw _currentException;
     
                return _result;
            }


Мы им воспользуемся в реализации EndEnumDirectories, суть происходящего сводится к … дождаться выполнения ассинхронной операции если это нужно и вернуть результат выполнения кода если не возникло никаких осложнений.По сути мы выполнили условие пункта 4 при котором клиент не дождавшись выполнения асинхронной операции вызывает метод EndEnumDirectories (мы дождемся за него :))).
Последний штрих конструктор класса:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
            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:

<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
        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)
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
                var manager = new DirectoryInfoManager();
                var ar = manager.BeginEnumDirectories("D:\\", null, null);
     
                // некий полезный код
     
                IList<DirectoryDescriptor> directories = manager.EndEnumDirectories(ar);


2)
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
                var manager = new DirectoryInfoManager();
                var ar = manager.BeginEnumDirectories("D:\\", null, null);
     
                // некий полезный код
     
                while (!ar.IsCompleted)
                {
                    // некий полезный код
                }


IList<DirectoryDescriptor> directories = manager.EndEnumDirectories(ar);
3)
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
                var manager = new DirectoryInfoManager();
                var ar = manager.BeginEnumDirectories("D:\\", null, null);
     
                // некий полезный код
     
                if(!ar.AsyncWaitHandle.WaitOne(1000, false))
                {
                    // выводим прогресс работы
                }
     
                IList<DirectoryDescriptor> directories = manager.EndEnumDirectories(ar);


4)
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
                var manager = new DirectoryInfoManager();
                manager.BeginEnumDirectories("D:\\", result =>
                                                         {
                                                             IList<DirectoryDescriptor> directories = manager.EndEnumDirectories(result);
                                                        
                                                         }, null);
     
                // некий полезный код


В заключение: Реализацию DirectoryInfoAsyncResult, очень легко сделать параметризированной на основе generic. Пусть это будет факультативным заданием для тех кто смог дочитать до этого места :)

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

Powered by Invision Power Board (https://www.invisionboard.com)
© Invision Power Services (https://www.invisionpower.com)