На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: maxim84_
  
> Асинхронное программирование , часть 1 (продолжение)
    Давайте проанализируем результат того, что мы имеем по окончанию первой моей статьи. Мы легко вызываем метод асинхронно и имеем возможность проинформировать вызывающий код о результатах его работы, также мы позаботились о том, что бы иметь возможность обработать исключение, которое могло бы возникнуть в дочерном потоке. В конце прошлой статьи я обратил внимание, что код нужно синхронизировать и отправил читателя к статье PIL о cross-thread в GUI. Зачастую такой подход обоснован, но справедливости ради нужно заметить, что способ синхронизации описаный там не единственный, более того мной был упомянут компонет BackgroundWorker, который умеет работать с GUI и синхронизировать операции его обновления. Следующим нашим шагом будет введение автоматического маршалинга кода который выполняется ассинхронно, в поток GUI. Это безусловно несколько усложнит наш код, но сделает работу с нашим классом гораздо более удобной. Конечно я знаю .NET Framework облегчит нам жизнь и мы разберемся с механизмами позволяющими маршалить код с потока в поток.

    Перед тем как мы добавим поддержку маршализации в наш код, нам прийдется остановится на нескольких теоретических моментах, уверен без этого приведенный мной код не будет очевидным. Прежде всего нужно поговорить о классе SynchronizationContext и о его предназначении. Строго говоря этот класс предоставляет базовую функциональность по маршалингу. Классы наследники обычно реализуют возможность синхронизации вызовов для конкретной технологии. Такие классы имеются в WPF, WindowsForms, APS.NET. Возможно вы уже догадались для чего использовалось наследование. Конечно, что бы обеспечить нам с Вами, возможность работать с таким контекстом через базовый класс при этом не задумываться при написании компонентов с какой именно технологией мы имеем дело. Какой же базовый функционал несет в себе контекст синхронизации? Самыми важными являются метод Post и Send. По сути эти методы будут производить маршализацию. Базовый класс, имеет наивные реализации, например Post использует ThreadPool, а Send обычный синхронный вызов делегата, зато методы объявлены как виртуальные, что и позволяет классам наследникам определять свои собственные механизмы маршалинга.

    Вот то, как реализованы методы в классе SynchronizationContext:

    ExpandedWrap disabled
          public virtual void Post(SendOrPostCallback d, object state)
          {
              ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
          }
       
      Тут для вызова метода через делегат используется пул потоков.
          public virtual void Send(SendOrPostCallback d, object state)
          {
              d(state);
          }

    А тут как говорилось, просто идет прямой вызов посредством делегата. Не очень полезно как по мне 
    С наследниками же дело обстоит веселее, вот реализация методов из WindowsFormsSynchronizationContext, я чуть ниже поясню смысл.

    ExpandedWrap disabled
          public override void Post(SendOrPostCallback d, object state)
          {
              if (this.controlToSendTo != null)
              {
                  this.controlToSendTo.BeginInvoke(d, new object[] { state });
              }
          }
       
          public override void Send(SendOrPostCallback d, object state)
          {
              Thread destinationThread = this.DestinationThread;
              if ((destinationThread == null) || !destinationThread.IsAlive)
              {
                  throw new InvalidAsynchronousStateException(SR.GetString("ThreadNoLongerValid"));
              }
              if (this.controlToSendTo != null)
              {
                  this.controlToSendTo.Invoke(d, new object[] { state });
              }
          }


    Метод пост пользуется тем, что контекст имеет ссылку на Control из Windows Forms, каждый контрол как вы возможно знаете, предоставляет метод BeginInvoke, позволяющий запустить код в потоке GUI. Вобщем-то не хитро. Несмотря на многа "букаф" в методе Send тут ситуация не сложнее, когда контекст создается, он получает сылку на поток
    ExpandedWrap disabled
       this.DestinationThread = Thread.CurrentThread;
    это код из конструктора. Проверив, что с потоком все хорошо, следует тот же прием что и для метода Post, но в синхронном исполнении. Осталось узнать как контекст получает ссылку на контрол который будут "беспокоить" на самом деле из другого контекста, на этот раз контекста потока. Вот полный код конструктора, думаю должно быть понятно без дополнительных объяснений:

    ExpandedWrap disabled
      public WindowsFormsSynchronizationContext()
      {
          this.DestinationThread = Thread.CurrentThread;
          Application.ThreadContext context = Application.ThreadContext.FromCurrent();
          if (context != null)
          {
              this.controlToSendTo = context.MarshalingControl;
          }
      }


    Собственно можно было бы уже начать пользоваться этим чудом, посредством волшебного статического свойства Current возвращающего ссылку на контекст, но на самом деле нам нужно познакомится с еще одним семейством классов, которые облегачат нам работу с контекстами :)
    Первый класс это вспомогательный AsyncOperatonManager, он имеет свойство SynchronizatoinContext, возвращающий ссылку на текущий контекст синхронизации, а также фабричный метод CreateCommand, который возвращает объект AsyncOperation. Попробуем разобраться с тем как это поможет нам и с тем как это работает. Первое свойство AsyncOperatonManager SynchronizatoinContext, метод простой он проверяет существует ли установленый контекст синхронизации и если да, то возращает его, если нет, то возвращается дефолтная реализация SynchronizatoinContext. Это свойство просто экономит нам написание пары лишних строчек кода при обращению к контекту, например не нужно на null проверять. Метод CreateCommand создает комманду. Что за комманду давайте взглянем подробней.
    Команда имеет ссылку на текущий контекст синхронизации, команда позволяет сохранить некий пользовательский стейт плюс к этому команда имеет собственное состояние (выполнена или нет). Когда команда выполнена, она может к примеру сообщить об этом контексту, что бы он коректно осводил необходимые ресурсы. Вобщем алгоритм следующий вы создаете комманду вызывая метод CreateOpperation у AsyncOperatonManager и передаете (опционально, можно передать Null) пользовательский объект служащий параметром для ассинхронного метода, затем AsyncOperatonManager устанавливает комманде текущий контекст, устанавливает комманде поле _complited в false и дергает метод контекста OperationStarted. Дефолтный контекст имеет пустую реализацию, зато наследники такие как WindowsFormsSynchronizationContext могут определять собственную логику по отслеживанию операции маршалинга (скажу по секрету, что они этого не делают :)) по сути вызов этот пустой, только контектст для ASP.NET определяет собственную логику в этом месте. И так команда получена, но как с ее помощью маршалить вызовы посредством ассоциированого контекста? Все предельно просто каждая команда имеет пару уже знакомых нам методов Post и Send которые делегируют всю нужную работу ассоциированному контексту. Единственные дополнениями к делегации, есть проверки, что делегат не null и собственное состояние комманды установлено в не выполнено. Команда в состоянии – выполнена, более не пригода к использованию. Это сделано для того, что бы коректно освобождать ресурсы.
    Еще один полезный метод который нужно описать это PostOperationComplited которые также делегирует вызов через Post, но дополнительно устанавливает команду в состояние выполнена.
    Теперь вооружившись общими знаниями, применем это все на практике по отношению к нашему собственому компоненту.
    Объявим переменную
    ExpandedWrap disabled
      private AsyncOperation _currentOperation;

    Я буду реализовывать модель по которой клиентский код не допускает несколько паралельных ассинхронных вызовов.
    В EnumAvailableSqlServersAsync инициализируем команду
    Вот тело метода:
    ExpandedWrap disabled
                 _currentOperation = AsyncOperationManager.CreateOperation(null);
       
       
                  ThreadPool.QueueUserWorkItem(obj =>
                   {
                       Exception execption = null;
                       IList<NetworkInstanceDesceriptor> result = null;
       
                       try
                       {
                           result = EnumAvailableSqlServers();
                       }
                       catch (Exception ex)
                       {
                           execption = ex;
                       }
                       finally
                       {
                           CombineResults(result, execption);
                       }
                   });


    Несложно заметить, что в методе произошли изменения, вместо непосредственной генерации события об окончании выполнения, я вызываю метод CombineResult назначение которого собрать EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>> и посредством команды выполнить маршализацию в поток GUI посредством PostOperationCompleted который вызывается у нашей комманды, ниже реализация этого метода:

    ExpandedWrap disabled
              private void CombineResults(IList<NetworkInstanceDesceriptor> result, Exception exception)
              {
                  var args = new EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>(result, exception, false, null);
                  _currentOperation.PostOperationCompleted(_onCompletedDelegate, args);
              }


    Первым параметром используется делегат SendOrPostCallback делегат объявлен как принимающий методы возвращающие void и принимающий параметром object. В этом методе мы еще находимся в коде который вызывается через пул, а вот после маршализации, то есть когда вызовется метод назначенный делегату мы окажемся в потоке GUI (если конечно наш код выполняется в WinForms или WPF)

    Нам нужно объявить такой делегат в программе:

    ExpandedWrap disabled
      private readonly SendOrPostCallback _onCompletedDelegate;


    и в конструкторе класса проинициализировать его:

    ExpandedWrap disabled
              public SqlServerNetworkManager()
              {
                  _onCompletedDelegate = new SendOrPostCallback(AsyncCallCompleted);
              }


    Метод который используется для инициализации:

    ExpandedWrap disabled
              private void AsyncCallCompleted(object operationState)
              {
                  var e = operationState as EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>;
                  InvokeEnumAvailableSqlServersComplited(e);
              }


    Внутри этого метода мы уже в потоке GUI!!! (если конечно наш код выполняется в WinForms или WPF :))


    То есть мы получаем уже маршализированное значение и все, что осталось сделать это сгенерировать соответствующие событие для уведомления клиента о том, что операция выполнена.

    Последний штрих добавление свойства IsBusy.

    Добавим управляющий флаг и свойство.

    ExpandedWrap disabled
      private bool _isBusy;
       
              public bool IsBusy
              {
                  get { return _isBusy; }
                  protected set { _isBusy = value; }
              }


    Когда вызывается EnumAvailableSqlServersAsync мы проверяем, что код не занят:
    ExpandedWrap disabled
                  if (IsBusy)
                      throw new InvalidOperationException();
       
                  IsBusy = true;


    и устанавливаем IsBusy в true,

    после того же как маршализация выполнена, мы можем снова разрешить пользоваться методом EnumAvailableSqlServersAsync


    ExpandedWrap disabled
              private void CombineResults(IList<NetworkInstanceDesceriptor> result, Exception exception)
              {
                  var args = new EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>(result, exception, false, null);
                  _currentOperation.PostOperationCompleted(_onCompletedDelegate, args);
                  IsBusy = false;
              }


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

    Ниже код с небольшими комментариями:

    ExpandedWrap disabled
          public class SqlServerNetworkManager : ISqlServerNetworkManager
          {
              private bool _isBusy;
              private readonly SendOrPostCallback _onCompletedDelegate;
       
              // используется для маршализации обращений между потоками
              private AsyncOperation _currentOperation;
       
              // подписавшись на событие клиент получит результат обработать резульатт или узнать об ошибке
              public event EventHandler<EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>> EnumAvailableSqlServersComplited;
       
              public SqlServerNetworkManager()
              {
                  _onCompletedDelegate = new SendOrPostCallback(AsyncCallCompleted);
              }
       
              public bool IsBusy
              {
                  get { return _isBusy; }
                  protected set { _isBusy = value; }
              }
       
              private void AsyncCallCompleted(object operationState)
              {
                  // Находимся здесь после синхронизации, можем без проблем обращаться к GUI.
                  var e = operationState as EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>;
                  InvokeEnumAvailableSqlServersComplited(e);
              }
       
              // Синхронная реализация
              public IList<NetworkInstanceDesceriptor> EnumAvailableSqlServers()
              {
                  var desceriptors = new List<NetworkInstanceDesceriptor>();
                  using (DataTable table = SmoApplication.EnumAvailableSqlServers(false))
                  {
                      foreach (DataRow row in table.Rows)
                          desceriptors.Add(BuildNetworkDescriptor(row));
                  }
       
                  return desceriptors;
              }
       
              // Ассинхронная реализация
              public void EnumAvailableSqlServersAsync()
              {
                  // Не даем обращаться к коду до конца выполнения маршализации,
                  // клиенту предоставляется свойство IsBusy, для того,
                  // что бы тот мог проверить возможность вызова
                  if (IsBusy)
                      throw new InvalidOperationException();
       
                  IsBusy = true;
       
                  _currentOperation = AsyncOperationManager.CreateOperation(null);
       
                  ThreadPool.QueueUserWorkItem(obj =>
                   {
                       Exception execption = null;
                       IList<NetworkInstanceDesceriptor> result = null;
       
                       try
                       {
                           result = EnumAvailableSqlServers();
                       }
                       catch (Exception ex)
                       {
                           execption = ex;
                       }
                       finally
                       {
                           CombineResults(result, execption);
                       }
                   });
                
              }
       
              // Собираем результат работы кода в аргумент события и маршализируем его между потоками
              private void CombineResults(IList<NetworkInstanceDesceriptor> result, Exception exception)
              {
                  var args = new EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>(result, exception, false, null);
                  _currentOperation.PostOperationCompleted(_onCompletedDelegate, args);
                  IsBusy = false;
              }
       
              private static NetworkInstanceDesceriptor BuildNetworkDescriptor(DataRow row)
              {
                  return new NetworkInstanceDesceriptor(row["Name"] as string,
                                                        row["Server"] as string,
                                                        row["Instance"] as string);
              }
       
              protected void InvokeEnumAvailableSqlServersComplited(EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>> e)
              {
                  EventHandler<EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>> complited = EnumAvailableSqlServersComplited;
                  if (complited != null)
                      complited(this, e);
              }
          }


    А теперь код клиента:

    ExpandedWrap disabled
         public partial class MainForm : Form
          {
              private readonly ISqlServerNetworkManager _manager = new SqlServerNetworkManager();
              public MainForm()
              {
                  InitializeComponent();
                  _manager.EnumAvailableSqlServersComplited += ManagerEnumAvailableSqlServersComplited;
              }
       
              protected void ManagerEnumAvailableSqlServersComplited(object sender, EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>> e)
              {
                  comboBoxServers.DataSource = e.Result;
                  comboBoxServers.DisplayMember = "Name";
              }
       
              private void buttonRefresh_Click(object sender, EventArgs e)
              {
                  if (_manager.IsBusy)
                      return;
       
                  _manager.EnumAvailableSqlServersAsync();
              }
          }


    Написав не так много кода, мы обеспечили клиенту очень удобную ассинхронную модель взаимодействия с нашим кодом, не требующую синхронизации со стороны клиента.

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


    Рейтинг@Mail.ru
    [ Script execution time: 0,0912 ]   [ 15 queries used ]   [ Generated: 19.04.24, 22:10 GMT ]