На главную
ПРАВИЛА FAQ Помощь Участники Календарь Избранное DigiMania RSS
msm.ru
Модераторы: maxim84_, juice
  
> Асинхронное программирование, часть 1
    Сегодня, и вполне возможно несколько следующих раз будем говорить о написании Concurrent кода.
    На мысль о том, что народу может быть интерессно меня натолкнуло последнее собеседование, где кандидат,
    который в принципе не имел нормальной базы в данной области,
    был не в состоянии аргументировать принимаемые им при разработке приложений решения.
    Его знания строились на некоторых предположениях, домыслах и естественно на шишках которые ему пришлось набить,
    но все это не гарантирует понимания и фундаментальных знаний. Обычный путь, после нахождения некого обходного пути
    – это решение превозносится как единственно правильное и безоговорочно верное. Конечно никто из нас не идеален и каждый из нас,
    может легко признать нехватку знаний относительно различных областей в программировании, однако я посчитал,
    что если я могу поделиться своим небольшим опытом, то другие возможно захотят поделиться своим.
    Наверное многие из вас слышали об АРМ (Asynchronous Programming Model) это один из известных паттернов для реализации
    выполнения ассинхронного кода на платформе .NET. Конкретно о нем мы поговорим в следующий раз, а сегодня мы поговорим
    о возможной альтернативе, другой модели ассинхронного выполнения которая построена на использовании событийной модели.
    События тут используются для уведомления вызывающего кода.
    В самом простом случае это код который выполняется в создаваемом нами дополнительном потоке и по окончании выполнения
    уведомляющий родительский поток об окончании своей работы,
    в следствии чего родительский поток имеет возможность воспользоваться результатами труда потока дочернего.
    Особо любопытные могут поинтересоваться о том, какую именно модель стоит использовать при разработке их приложений
    ту о которой будем говорить или APM? Я считаю, что это не просто правильный вопрос он более, чем важен для понимания того,
    что мы используем и как мы пишем наш код. Однозначно верного ответа нет, каждая модель имеет свои преимущества и недостатки,
    но обобщенное правило все же существует. Если клиент твоего кода библиотека то обычно используется APM,
    если графический интерфейс то вероятно правильный выбор будет реализация ассинхронных вызовов основаных на событийной модели.
    Типичным представителем рассматриваемой нами модели есть общеизвестный BackgroundWorker класс. Сколько же времени он съэкономил нам!?
    Мне много, он выручал меня и в те времена когда многопоточное программирование было для меня тайной за семью замками,
    он же облегачал мою работу еще много и много раз и я уверен еще не раз выручит. Но суть разговора
    не будет сводиться к BackgroundWorker это было бы унизительно для нас с Вами мы умеем им пользоваться :),
    Мы же поговорим о том, как правильно писать
    свой код реализуя классы подобные классу BackgroundWorker.
    Я не стану останавливаться на проблемах синхронизации cross-thread и GUI операций – сегодня коснемся их вскользь,
    мы же посвятим свое время написанию реально полезного кода, который продемонстрирует на практике использование описываемого мной паттерна,
    а также поможет лучше понять, что может предложить нам в помощь .NET Framework в частности и как нужно стараться писать свой код,
    Приступим!? Начнем с рассмотрения довольно типичной задачи, которая возникает перед программистом
    , - инициализация формы данными получение которых требует значительного времени.
    Довольно типична ситуация когда мы видим приложения или формы которые, как бы подвисают в момент выполнения некоторых,
    запрашиваемых пользователем действий. Что бы понять почему так происходит, достаточно просто понимать
    , что GUI в Windows всегда выполняется в единственном ассоциированом с ним потоке. Каждая презентационная технология в рамках .NET
    реализует специальный внутренний менеджер который неустанно проверяет и пресекает все наши попытки работать с GUI из других потокав.
    Мы можем иметь много потоков, но взаимодействывать с GUI можем только в одном единственном.
    Можно долго рассуждать почему это именно так и кто в этом виноват, но это данность с которой мы вынуждены мириться.
    Могу только заметить, что это не всегда плохо и такая модель предлагает некоторые бенефиты.
    Так вот как только приложение выполняет код который занимает какое либо значимое время из потока GUI,
    GUI перестает обрабатывать все поступающие ему сообщения, выполнение потока по сути приостанавливается до конца выполнения этого кода,
    что приводит к отсутствию перерисовки, «подвисанию» и появлению различных артефактов. Далеко за примером я ходить не буду.
    Думаю каждый из нас хоть раз да пользовался Data Connection – Add Connection мастером в студийном Server Explorer,
    после попытки выбрать сервер из комбобокса, окно уходит в «даун», а если попытаться подергать
    хидер появляется устрашающая надпись о том, что приложение не отвечает на запросы ...
    Конечно мы знаем с вами, что это не так и в конце концов нужный код выполниться и мы сможем выбрать нужный нам сервер из списка.
    Причина происходящего думаю для Вас уже очевидно, в момент опроса сети на наличие экземпляров было «заморожено» выполнение потока GUI
    и приложение не было способно сделать, что ли бо полезное. Мне не нравятся такие приложения, а Вам? Гораздо лучше,
    стараться держать поток GUI не загруженным. С точки зрения идеалиста, можно было бы сказать, что идеальный GUI выполняет в GUI потоке
    исключительно обращения к GUI эллементам (чтение, запись) и синхронизацию,
    весь же остальной код должен был бы выполняться в параллельных потоках.
    Мир не идеален, но мы можем постараться сделать так, что бы он хотя бы чуть чуть походил на такой. Что бы решить вышепреведенную проблему.
    Достаточно код который должен опросить сеть выполнить в независимом от GUI потоке, потом сообщить основному потоку, что код выполнился,
    передать результат выполнения кода в основной поток, произвести синхронизацию и обновить данные в контролах. Звучит устрашающе,
    но я помогу разобраться с первой частью проблемы, а известная статья PIL поможет разобраться с синхронизацией и объновлением.
    Все действительно просто.

    Давайте определим сначала наше синхронное API.

    ExpandedWrap disabled
          public interface ISqlServerNetworkManager
          {
              IList<NetworkInstanceDesceriptor> EnumAvailableSqlServers();
          }


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

    ExpandedWrap disabled
          public class NetworkInstanceDesceriptor
          {
              public NetworkInstanceDesceriptor(){}
       
              public NetworkInstanceDesceriptor(string name, string server, string instance)
              {
                  Name = name;
                  Server = server;
                  Instance = instance;
              }
       
              public string Instance { get; set; }
              public string Server { get; set; }
              public string Name { get; set; }
       
              public override string ToString()
              {
                  return String.Format("Name: {0} Server: {1} - Instance: {2}", Name, Server, Instance ?? "Default");
              }
          }


    Сейчас же мы имплементируем данный интерфейсс. .NET предлагает несколько путей того, как мы можем выполнить перечисление серверов.
    Я воспользуюсь возможностями SMO. Для того, что бы код компилировался вам, нужно подключить сборку Microsoft.SqlServer.Smo
    в проект и добавить соответствующее пространство имен.
    И так реализация:

    ExpandedWrap disabled
          public class SqlServerNetworkManager : ISqlServerNetworkManager
          {
              public IList<NetworkInstanceDesceriptor> EnumAvailableSqlServers()
              {
                  var descriptors = new List<NetworkInstanceDesceriptor>();
                  using (DataTable table = SmoApplication.EnumAvailableSqlServers())
                  {
                      foreach (DataRow row in table.Rows)
                          descriptors.Add(BuildNetworkDescriptor(row));
                  }
       
                  return descriptors;
              }
       
              private static NetworkInstanceDesceriptor BuildNetworkDescriptor(DataRow row)
              {
                  return new NetworkInstanceDesceriptor(row["Name"] as string,
                                                        row["Server"] as string,
                                                        row["Instance"] as string);
              }
           }


    Синхронное API реализовано. В нем нет ничего из области Rocket Since,
    после выполнения запроса, мы обрабатываем полученный DataTable построчно,
    получая на выходе нужную нам коллекцию.

    Программисты реализовывавшие выше описаный визард, не особо заморачивались. Они вызвали синхронный метод, установили курсор в часики
    и ... получили результат, заполнили комбобокс, сбросили курсор в дефолт и двинулись дальше. Так написано 90% всего программного обеспечения,
    правда среди этих 90% всегда находяться те 10%, где такой подход выглядит невежественным.

    Следующим этапом,будет добавление ассинхронного метода. Давайте подумаем, что нам для этого потребуется,
    прежде всего сам метод который приведет к ассинхронному вызову, а также событие подписавшийсь
    на которое клиент класса получит уведомление о том,
    что код выполнен и сможет через аргументы события получить результат выполнения этого самого кода. И так метод:

    ExpandedWrap disabled
      void EnumAvailableSqlServersAsync()


    ,мы ничего прямо не возвращаем клиенту потому тип возврата void, так же наш синхронный метод не требовал аргументов,
    потому и в нашем случае они не требуются. Обратите внимание на окончание в названии метода - Async, это правило хорошего тона,
    программист который будет пользоваться нашим API, сможет сразу определить, что вызов этого метода приводит к ассинхронной операции,
    а следовательно, вполне возможно ему потребуется подписаться на событие, что бы получить результат работы данного метода.
    Предварительно отметим,
    что в этом методе мы и будем создавать дополнительный поток, в котором и будет вызываться наша синхронная реализация.
    Добрались наши руки и события. .NET Framework, предоставляет стандартный базовый класс для аргумента, который должен использоваться
    в данном случае это – AsyncCompletedEventArgs. Я просто настоятельно рекомендую пользоваться им. Самое любопытное,
    что большинство из встреченых мной программистов, не знает о существовании подобного класса.
    Сейчас я остановлюсь на двух важных свойствах определенных в данном типе это Canceled и Error.
    Первое позволяет обработчику узнать, что выполнение потока было прервано, по желанию пользователя
    (мы могли бы реализовать метод, который позволил бы нам отменять выполнение ассинхронной операции),
    второе предоставляет возможность коду, что выполняется в дочернем потоке сообщить родительскому
    об исключении произошедшем при его выполнении.
    Это важно. Это очень, очень важно…

    Тут мы сделаем короткий экскурс в модель обрабоки исключений при работе с потоками.
    Проведем один небольшой эксперемент.

    ExpandedWrap disabled
                      static void Main(string[] args)
              {
                  try
                  {
                      ThrowExceptionMethod(null);
                  }
                  catch
                  {
                      
                  }
       
                  Console.ReadLine();
              }
       
              private static void ThrowExceptionMethod(object o)
              {
                  throw new Exception("Child thread exception");
              }


    Мы «скушали» исключение, и программа продолжает работать. Суть в другом, мы могли не «есть» исключение, а коректно его обработать
    и далее востановить нормальную работу приложения, либо же если ошибка была фатальной завершить приложение в мягкой для него форме.

    Давайте вызовем, ThrowExceptionMethod из другого потока:

    ExpandedWrap disabled
              static void Main(string[] args)
              {
                  try
                  {
                      ThreadPool.QueueUserWorkItem(ThrowExceptionMethod);
                  }
                  catch(Exception ex)
                  {
                      
                  }
       
                  Console.ReadLine();
              }
       
              private static void ThrowExceptionMethod(object o)
              {
                  throw new Exception("Child thread exception");
              }


    Cathc не возымел никакого действия. Теперь давайте вспомним о том, что необработанное исключение в порожденных потоках,
    приводят к фатальному сбою в приложении. В .NET как мы только, что убедились существуют механизмы которые мешают вам из главного потока
    подавить необработанные исключения в дочернем потоке. Единственное, что вам остается подписаться на AppDomain.UnhandledException,
    но это может оказаться полезным разве, что для логирования. Востановить работу приложения всеравно не удасться.
    Следовательно обработка исключений в дочеренем потоке является обязательной если вы не желаеете,
    что бы ваше приложение вываливалось с ошибкой!
    Что бы посмотреть на эффект обработки исключения в дочернем потоке давайте «скушаем» исключение в уже в нем.

    ExpandedWrap disabled
      class Program
          {
              static void Main(string[] args)
              {
                  try
                  {
                      ThreadPool.QueueUserWorkItem(ThrowExceptionMethod);
                  }
                  catch(Exception ex)
                  {
                      
                  }
       
                  Console.ReadLine();
              }
       
              private static void ThrowExceptionMethod(object o)
              {
                  try
                  {
                      throw new Exception("Child thread exception");
                  }
                  catch(Exception ex)
                  {
                      
                  }
              }
          }


    Приложение продолжило работать, единственное, что надо запомнить раз и на всю жизнь, что "глотать"
    исключения не хорошо их всегда нужно обрабатывать или пробрасывать выше, если ваш код библиотека.

    Возвращаясь к AsyncCompletedEventArgs и к свойству его Error, то оно предоставляет удобный путь дочернему потоку обработать
    исключение и выслать его «посылкой» главному потоку. Я буду наследовать этот класс т.к. я хочу добавить возможность передавать еще дополнительно
    результат выполнения метода в нашем случае это IList<NetworkInstanceDesceriptor>. Паралельно хотелось бы сделать так,
    что бы этот наследник был более универсальным и мог использоваться с различными типами возврата при написании любого ассинхронного API,
    хотя решение и без Generic имеет смысл во многих кейсах, когда разрабатывается специализированные библиотеки.

    ExpandedWrap disabled
          public class EnumAvailableSqlServersEventArgs<T> : AsyncCompletedEventArgs
          {
              public EnumAvailableSqlServersEventArgs(T result, Exception error, bool cancelled, object userState)
                  : this(error, cancelled, userState)
              {
                  Result = result;
              }
       
              public EnumAvailableSqlServersEventArgs(Exception error, bool cancelled, object userState) : base(error, cancelled, userState)
              {
              }
       
              public T Result { get; set; }
          }


    Ну, что пришло время расширить наш интерфейс:
    Теперь он выглядит так:

    ExpandedWrap disabled
          public interface ISqlServerNetworkManager
          {
              event EventHandler<EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>> EnumAvailableSqlServersComplited;
              IList<NetworkInstanceDesceriptor> EnumAvailableSqlServers();
              void EnumAvailableSqlServersAsync();
          }


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

    ExpandedWrap disabled
              protected void InvokeEnumAvailableSqlServersComplited(EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>> e)
              {
                  EventHandler<EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>> complited = EnumAvailableSqlServersComplited;
                  if (complited != null)
                      complited(this, e);
              }


    А метод который запускает код в отдельном потоке, выглядит следующим образом:

    ExpandedWrap disabled
             public void EnumAvailableSqlServersAsync()
              {
                  ThreadPool.QueueUserWorkItem(o =>
                                           {
                                               try
                                               {
                                                   IList<NetworkInstanceDesceriptor> result = EnumAvailableSqlServers();
                                                   InvokeEnumAvailableSqlServersComplited(new EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>(result,
                                                   null,
                                                   false,
                                                   null));
                                               }
                                               catch (Exception ex)
                                               {
                                                   InvokeEnumAvailableSqlServersComplited(new EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>(ex, false,
                                                                                                                    null));
                                                }
                                            });
              }


    Как Вы можете видеть я использовал ThreadPool, вместо создания потока в вручную, я почти всегда предпочитаю делать именно так.
    Вместо же использования явной реализации метода удовлетворяющего WaitCallback, я использую лямбда выражение,
    на мой взгляд код не только короче, но и понятнее. Реализация через ThreadPool,
    прямолинейна, но имеет ряд ограничений, тем не менее обычно этого достаточно для приложения главное это просто и безопасно,
    а при надобности всегда можно изменить реализацию на более сложную. Отдельно стоит отметить,
    что реализация возможности множественных вызовов ассинхронного метода, в этой нашей версии никак не обрабатывается,
    вообщето было бы не плохо это сделать. Я обозначу то, как это делается.
    Первый вариант через управляющий флаг, что-то вроде IsBusy или InProgress это даст клиенту класса возможность
    проверить возможность повторного обращения,
    в свою очередь код обнаруживая множественные вызовы, может бросать исключение.
    Другой более не тривиальный способ состоит в том, что бы передавать, в Async метод дополнительный параметр, уникально идентифицирующий вызов,
    что то похожее на object taskId. В свою очередь при генерации события для клиента класса, этот taskId, может быть передан обработчику.
    Дополнительный плюс состоит в том, что в этом сценарии, можно использовать данный идентификатор
    при реализации возможностей по отмене конкретных "таск".

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

    ExpandedWrap disabled
              static void Main(string[] args)
              {
                  var manager = new SqlServerNetworkManager();
                  manager.EnumAvailableSqlServersComplited += ManagerEnumAvailableSqlServersComplited;
                  manager.EnumAvailableSqlServersAsync();
                  Console.ReadLine();
              }
       
              static void ManagerEnumAvailableSqlServersComplited(object sender, EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>> e)
              {
                  if (e.Error == null)
                  {
                      foreach (var instance in e.Result)
                          Console.WriteLine(instance);
                  }
                  else
                  {
                      Console.WriteLine("Показуем пользователю красивое окошко сообщающие, что случилось, что-то не доброе :)");
                  }
              }


    Подписываемся на событие, вызываем ассинхронный метод. Обработчик получает результат и выводит его на консоль…
    Желаю Вам всем успеха. Напоследок хочу напомнить, что когда будете использовать эту модель при работе с GUI,
    не забудьте синхронизировать доступ об этом очень хорошо и подробно написал PIL в своей давешней, но от того не менее актуальной статье.
    (Продолжение следует)
    Ни один победитель не верит в случайность.
      Если уже пошло на то, то немного бы хотел пофлеймить на тему "события vs колбэки"
      Вот 2 небольших на мой взгляд недостатка в применении событий в асинхронном программировании
      1. Если например используеться одна ссылка на объект SqlServerNetworkManager, то нужно внимательно следить чтобы не подписаться на событие дважды
      И к тому же лишнее количество строчек добавляют события. Красивей на мой взгляд было бы так:
      ExpandedWrap disabled
          manager.EnumAvailableSqlServersAsync(ManagerEnumAvailableSqlServersComplited);

      2. В потоке нужно проверять подписан ли кто-то на событие, тоже лишний код типа:
      ExpandedWrap disabled
          EventHandler del = MyEvent;
          if (del != null)
              del(this, EventArgs.Empty);


      Я конечно не могу опровергать использование событий в данных асинхронных конструкциях, поскольку не являюсь продвинутым специалистом в этой области, просто хотел бы узнать мнение таковых :)
      Сообщение отредактировано: 2005fs -
        2005fs
        1. прочитай вторую часть (продолжение первой).
        2. перечитай начало первой. Есть два подхода, один ориетирован на работу с GUI, второй с библиотеками.

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


        Рейтинг@Mail.ru
        [ Script Execution time: 0,1204 ]   [ 17 queries used ]   [ Generated: 25.09.17, 04:32 GMT ]