На главную
ПРАВИЛА FAQ Помощь Участники Календарь Избранное DigiMania RSS
msm.ru
Модераторы: maxim84_, juice
  
> Асинхронное программирование, часть 2 APM (Asynchronous Programming Model)
    В прошлой статье посвященной асинхронной модели программирования мы рассмотрели шаблон базирующийся на событиях и я пообещал в следующей своей статье рассмотреть альтернативную модель которая использует APM (Asynchronous Programming Model) паттерн. Как было сказано в предисловии к первой статье данный подход нацелен на потребителей которым является библиотечный код. Мы не будем пытаться больше синхронизировать наш код с потоком в котором выполняется GUI приложения, а в замен получим большую гибкость и несколько возможных сценариев получения результатов выполнения кода. Начнем. Для тестирования нашего кода используем следующую программную модель.
    Класс DirectoryDescriptor

    ExpandedWrap disabled
          public class DirectoryDescriptor
          {
              public string Name { get; set; }
              public string Path { get; set; }
          }


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

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

    ExpandedWrap disabled
      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 будет следующим:

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

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

    ExpandedWrap disabled
      ТипВозвратКакВсинхронойВерсии EndXXX(IAsyncResult result);


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

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

    ExpandedWrap disabled
              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 потребует этого в обязательном порядке.
    Сам интерфейс реализуется тривиальнейшим способом:

    ExpandedWrap disabled
              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; } }


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

    ExpandedWrap disabled
      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:

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


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

    ExpandedWrap disabled
              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:

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


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


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


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


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

    До новых встреч.
    Ни один победитель не верит в случайность.
    0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
    0 пользователей:


    Рейтинг@Mail.ru
    [ Script Execution time: 0,1358 ]   [ 17 queries used ]   [ Generated: 20.11.17, 19:25 GMT ]