Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
Iceсream Screen Recorder – это программа для захвата видео с аудио и микрофоном и создания скриншотов с помощью ряда доп...
SlimDrivers — бесплатная программа для обнаружения присутствующих на компьютере драйверов, определение их версий, поиска...
Многофункциональный, кроссплатформенный текстовый редактор с функциями подсветки синтаксиса, авто-подстановкой выражений...
Программа позволяет быстро обнаруживать на раннем этапе возможные отказы в работе сети или веб-сайта. PRTG Network Monit...
Программа для создания презентаций и интерактивных обучающих видеоуроков. Camtasia Studio может осуществлять захват изоб...
OSzone.net Microsoft Разработка приложений Другое Многопоточность и диспетчеризация в MVVM-приложениях RSS

Многопоточность и диспетчеризация в MVVM-приложениях

Текущий рейтинг: 0 (проголосовало 0)
 Посетителей: 681 | Просмотров: 909 (сегодня 1)  Шрифт: - +

Примерно год назад я начал серию статей по шаблону Model-View-ViewModel (MVVM) для веб-сайта MSDN Magazine (все они доступны по ссылке is.gd/mvvmmsdn). В этих статьях было показано, как использовать компоненты MVVM Light Toolkit для создания слабо связанных приложений в соответствии с данным шаблоном. Я исследовал встраивание зависимостей (dependency injection, DI) и шаблоны IOC-контейнеров (Inversion of Control) (в том числе MVVM Light SimpleIoc), рассказал о Messenger и обсудил сервисы View, такие как Navigation, Dialog и др. Я также продемонстрировал, как создавать данные этапа проектирования, чтобы в максимально полной мере задействовать визуальные дизайнеры вроде Blend, и поговорил о компонентах RelayCommand и EventToCommand, заменяющих обработчики событий для ослабления связи между View и его ViewModel.

В этой статье я хочу рассмотреть еще один распространенный сценарий в современных клиентских приложениях: обработку нескольких потоков и обеспечение взаимодействия между ними. Многопоточность становится все важнее в современных платформах приложений, таких как Windows 8, Windows Phone, Windows Presentation Foundation (WPF), Silverlight и др. На каждой из этих платформ — даже на наименее мощной — необходимо запускать фоновые потоки и управлять ими. По сути, на малых платформах с гораздо меньшими вычислительными ресурсами это даже важнее, чтобы предоставлять улучшенную пользовательскую среду (user experience, UX).

Хороший пример — платформа Windows Phone. В самой первой версии (Windows Phone 7) было довольно трудно добиться плавной прокрутки в длинных списках, особенно когда шаблоны элементов содержали изображения. Однако в более поздних версиях декодирование изображений, а также обработка некоторых анимаций передается выделенному фоновому потоку. В итоге, когда загружается изображение, это больше не влияет на основной поток, и прокрутка остается плавной.

Этот пример подчеркивает некоторые важные концепции, которые я буду исследовать в этой статье. Начну с обзора того, как многопоточность в целом работает в XAML-приложениях.

Проще говоря, поток можно считать меньшей единицей выполнения приложения. Каждому приложению принадлежит минимум один поток, который называется основным (main thread). Этот поток запускается операционной системой, когда вызывается метод main приложения; это происходит при старте приложения. Заметьте, что более-менее одно и то же происходит на всех поддерживаемых платформах — как в WPF, работающей на мощных компьютерах, так и на устройствах под управлением Windows Phone с ограниченными вычислительными ресурсами.

При вызове какого-либо метода операция добавляется в очередь. Каждая операция выполняется последовательно, в соответствии с порядком, в котором она была добавлена в очередь (хотя на порядок выполнения операций можно повлиять, назначив им приоритеты). Объект, отвечающий за управление очередью, называется диспетчером потока (thread’s dispatcher). Этот объект является экземпляром класса Dispatcher в WPF, Silverlight и Windows Phone. В Windows 8 объект диспетчера называется CoreDispatcher и использует несколько иной API.

При необходимости приложение может запускать новые потоки — как явным образом в коде, так и неявным, с помощью некоторых библиотек или ОС. В основном цель запуска нового потока — выполнение какой-либо операции (или ожидание результата этой операции) без блокирования остальной части приложения. Это может делаться для операции, требующей интенсивных вычислений, ввода-вывода и т. д. Многопоточность современных приложений постоянно расширяется из-за роста требований к качественной UX. Поскольку приложения становятся более сложными, увеличивается количество запускаемых ими потоков. Хороший пример такой тенденции — инфраструктура Windows Runtime, применяемая в приложениях Windows Store. В этих современных клиентских приложениях очень распространены асинхронные операции (т. е. операции, выполняемые фоновыми потоками). Так, обращение к каждому файлу в Windows 8 теперь является асинхронной операцией. Вот как в WPF считывается файл (синхронно):

public string ReadFile(FileInfo file)
{
  using (var reader = new StreamReader(file.FullName))
  {
    return reader.ReadToEnd();
  }
}

А это эквивалентная асинхронная операция в Windows 8:

public async Task<string> ReadFile(IStorageFile file)
{
  var content = await FileIO.ReadTextAsync(file);
  return content;
}

Обратите внимание на присутствие ключевых слов await и async в версии для Windows 8. Они предназначены для того, чтобы избегать использования обратных вызовов в асинхронных операциях и чтобы код было легче читать. Здесь они нужны, поскольку файловая операция асинхронная. В WPF-версии, напротив, эта операция синхронная, что создает риск блокировки основного потока, если считываемый файл окажется длинным. Это может вызвать сделать анимации прерывистыми или привести к тому, что UI перестанет обновляться, т. е. ухудшить UX.

Аналогично длительные операции в вашем приложении должны выноситься в фоновые потоки, если есть риск того, что они будут мешать обновлению UI. Например, в WPF, Silverlight и Windows Phone код на рис. 1 инициирует фоновую операцию, которая выполняется в длительном цикле. В каждом цикле поток ненадолго переводится в спящее состояние, чтобы дать время другим потокам обработать свои операции.

Рис. 1. Асинхронная операция в Microsoft .NET Framework

public void DoSomethingAsynchronous()
{
  var loopIndex = 0;
  ThreadPool.QueueUserWorkItem(
    o =>
    {
      // Это фоновая операция!
      while (_condition)
      {
        // Здесь что-то делаем
        // ...
        // Засыпаем ненадолго
        Thread.Sleep(500);
      }
  });
}

Взаимодействие потоков

Когда одному из потоков нужно взаимодействовать с другим, следует соблюдать некоторые меры предосторожности. Например, я модифицирую код с рис. 1 так, чтобы он отображал сообщение о состоянии в каждом цикле. Для этого я просто добавлю строку кода в цикл while, которая устанавливает свойство Text элемента управления StatusTextBlock, находящегося в XAML:

while (_condition)
{
  // Здесь что-то делаем
  // Уведомляем пользователя
  StatusTextBlock.Text = string.Format(
    "Loop # {0}", loopIndex++);
  // Ненадолго засыпаем
  Thread.Sleep(500);
}

Приложение SimpleMultiThreading в коде, сопутствующем этой статье, демонстрирует этот пример. Если вы запускаете его, используя кнопку Start (crashes the app), то в приложении действительно происходит крах. Что же случилось? Когда объект создается, его владельцем становится тот поток, в котором был вызван метод конструктора. В случае UI-элементов объекты создаются анализатором XAML (XAML parser) при загрузке XAML-документа. Все это происходит в основном потоке. В итоге все UI-элементы принадлежат основному потоку, который также зачастую называют UI-потоком. Когда фоновый поток в предыдущем коде пытается модифицировать свойство Text элемента StatusTextBlock, обнаруживается недопустимое обращение между потоками. Как следствие, генерируется исключение. Это можно увидеть, выполняя данный код в отладчике. На рис. 2 показан диалог исключения. Обратите внимание на сообщение «Additional information», которое указывает корень проблемы.

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

Рис. 2. Диалог исключения, вызванного недопустимым обращением из одного потока в другой

Чтобы этот код работал, фоновый поток должен ставить операция в очередь в основном потоке через его диспетчер. К счастью, каждый FrameworkElement также является DispatcherObject, как иллюстрирует иерархия .NET-классов на рис. 3. Каждый DispatcherObject предоставляет свойство Dispatcher, которое дает доступ к своему диспетчеру. Таким образом, данный код можно модифицировать, как показано на рис. 4.

Иерархия классов

Рис. 3. Иерархия классов

Рис. 4. Диспетчеризация вызова UI-потоку

while (_condition)
{
  // Здесь что-то делаем
  Dispatcher.BeginInvoke(
    (Action)(() =>
    {
      // Уведомляем пользователя
      StatusTextBlock.Text = string.Format(
        "Loop # {0}", loopIndex++);
    }));
  // Ненадолго засыпаем
  Thread.Sleep(500);
}

Диспетчеризация в MVVM-приложениях

Когда фоновая операция запускается из ViewModel, ситуация несколько меняется. Обычно ViewModel не наследует от DispatcherObject. ViewModel — это POCO-объекты (Plain Old CLR Objects), которые реализуют интерфейс INotifyPropertyChanged. Например, на рис. 5 показан ViewModel, производный от класса ViewModelBase из MVVM Light. В истинном стиле MVVM я добавляю наблюдаемое свойство Status, генерирующее событие PropertyChanged. Затем из кода фонового потока я пытаюсь записать в это свойство информационное сообщение.

Рис. 5. Обновление связанного свойства в ViewModel

public class MainViewModel : ViewModelBase
{
  public const string StatusPropertyName = "Status";
  private bool _condition = true;
  private RelayCommand _startSuccessCommand;
  private string _status;
  public RelayCommand StartSuccessCommand
  {
    get
    {
      return _startSuccessCommand
        ?? (_startSuccessCommand = new RelayCommand(
          () =>
          {
            var loopIndex = 0;
            ThreadPool.QueueUserWorkItem(
              o =>
              {
                // Это фоновая операция!
                while (_condition)
                {
                  // Здесь что-то делаем
                  DispatcherHelper.CheckBeginInvokeOnUI(
                    () =>
                    {
                      // Направляем обратно в основной поток
                      Status = string.Format(
                        "Loop # {0}", loopIndex++);
                    });
                  // Ненадолго засыпаем
                  Thread.Sleep(500);
                }
              });
          }));
    }
  }
  public string Status
  {
    get
    {
      return _status;
    }
    set
    {
      Set(StatusPropertyName, ref _status, value);
    }
  }
}

Если запустить этот код в Windows Phone или Silverlight, он прекрасно работает, пока я не попытаюсь связать свойство Status с TextBlock в XAML UI. Выполнение операции вновь обрушит приложение. Как и раньше, едва фоновый поток попытается обратиться к элементу, принадлежащему другому потоку, возникнет исключение. Это происходит, даже если доступ осуществляется через механизм связывания с данными.

Заметьте, что в WPF дело обстоит иначе и код, приведенный на рис. 5, работает, даже если свойство Status связано с TextBlock. Это вызвано тем, что WPF автоматически направляет событие PropertyChanged в основной поток в отличие от всех остальных инфраструктур XAML. Во всех других инфраструктурах требуется специальное решение по диспетчеризации. По сути, требуется система, которая при необходимости диспетчеризует вызов. Чтобы код ViewModel можно было использовать и в WPF, и в других инфраструктурах и не заботиться о диспетчеризации, вы должны располагать объектом, который делал бы это автоматически.

Поскольку ViewModel является POCO, у него нет доступа к свойству Dispatcher, поэтому мне нужен другой способ обращения к основному потоку и постановки операции в очередь. Для этого предназначен компонент DispatcherHelper в MVVM Light. Фактически этот класс хранит Dispatcher основного потока в статическом свойстве и предоставляет несколько вспомогательных методов для доступа к нему удобным и единым способом. Чтобы класс мог работать, его нужно инициализировать в основном потоке. В идеале, это должно происходит на самом раннем этапе жизненного цикла приложения, чтобы его функциональность была доступна с момента запуска приложения. Как правило, DispatcherHelper в приложении MVVM Light инициализируется в App.xaml.cs — файле, определяющем стартовый класс приложения. В Windows Phone вы вызываете DispatcherHelper.Initialize в методе InitializePhoneApplication сразу после создания основной рамки окна приложения. В WPF этот класс инициализируется в конструкторе App. В Windows 8 вы вызываете метод Initialize в OnLaunched сразу после активизации окна.

По окончании вызова метода DispatcherHelper.Initialize свойство UIDispatcher класса DispatcherHelper содержит ссылку на диспетчер основного потока. Напрямую это свойство используют относительно редко, но при необходимости такое возможно. Однако лучше задействовать метод CheckBeginInvokeOnUi. Этот метод принимает делегат как параметр. Обычно вы используете лямбда-выражение, показанное на рис. 6, но вместо него можно подставить именованный метод.

Рис. 6. Использование DispatcherHelper для предотвращения краха

while (_condition)
{
  // Здесь что-то делаем
  DispatcherHelper.CheckBeginInvokeOnUI(
    () =>
    {
      // Направляем обратно в основной поток
      Status = string.Format("Loop # {0}", loopIndex++);
    });
  // Ненадолго засыпаем
  Thread.Sleep(500);
}

Как и предполагает название, этот метод сначала выполняет проверку. Если код, вызвавший этот метод, уже выполняется в основном потоке, никакой диспетчеризации не требуется. В таком случае делегат выполняется немедленно — прямо в основном потоке. Однако, если код, вызвавший этот метод, находится в фоновом потоке, происходит диспетчеризация.

Поскольку метод выполняет проверку перед диспетчеризацией, вызывающий может быть уверен, что код всегда будет использовать оптимальный вызов. Это особенно полезно, когда вы пишете кросс-платформенный код, где многопоточность может работать с небольшими различиями на разных платформах. В данном случае код ViewModel на рис. 6 можно использовать где угодно без модификации строки, в которой задается свойство Status.

Кроме того, DispatcherHelper абстрагирует различия в API диспетчера между платформами XAML. В Windows 8 основные члены CoreDispatcher — это метод RunAsync и свойство HasThreadAccess. Однако в других инфраструктурах XAML используются методы BeginInvoke и CheckAccess соответственно. Применяя DispatcherHelper, вам не придется заботиться об этих различиях, и сделать код общим будет проще.

Диспетчеризация на практике: датчики

Я проиллюстрирую применение DispatcherHelper, создав приложение Windows Phone для датчика компаса.

В сопутствующем этой статье коде есть набросок приложения под названием «CompassSample – Start». Откройте это приложение в Visual Studio и вы увидите, что доступ из MainViewModel к датчику компаса инкапсулирован в сервисе SensorService, который является реализацией интерфейса ISensorService. Эти два элемента вы найдете в папке Model.

MainViewModel получает ссылку на ISensorService в своем конструкторе и регистрируется на каждое изменение в показаниях компаса, вызывая метод SensorService RegisterForHeading. Этот метод требует обратного вызова, который будет выполняться всякий раз, когда датчик сообщает об изменении в направлении устройства под управлением Windows Phone. В MainViewModel замените конструктор по умолчанию следующим кодом:

sensorService.RegisterForHeading(
  heading =>
  {
    Heading = string.Format("{0:N1}°", heading);
    Debug.WriteLine(Heading);
  });

К сожалению, в эмуляторе Windows Phone нет возможности сымитировать аппаратный компас. Для проверки кода придется запустить приложение на физическом устройстве. Подключите это устройство и запустите код в режиме отладки, нажав F5. Наблюдайте за консольным окном Output в Visual Studio. Вы увидите вывод Compass. Если вы подвигаете устройство, то сможете найти север и наблюдать, как будет постоянно обновляться значение.

Затем я свяжу TextBlock в XAML со свойством Heading в MainViewModel. Откройте MainPage.xaml и найдите TextBlock в ContentPanel. Замените «Nothing yet» в свойстве Text на «{Binding Heading}». Если вы снова запустите приложение в режиме отладки, приложение рухнет с сообщением об ошибке, похожим на то, что вы видели ранее. И вновь это исключение из-за некорректного взаимодействия между потоками.

Ошибка возникает потому, что датчик компаса обрабатывается фоновым потоком. Когда запускается обратный вызов, он также выполняется в фоновом потоке, как и аксессор set свойства Heading. Поскольку TextBlock принадлежит основному потоку, генерируется исключение. Здесь тоже нужно создать «безопасную зону» — позаботиться о диспетчеризации операций в основной поток. Для этого откройте класс SensorService. Событие CurrentValueChanged обрабатывается методом CompassCurrentValueChanged; как раз в нем выполняется метод обратного вызова. Замените этот код показанным ниже, где используется DispatcherHelper:

void CompassCurrentValueChanged(object sender,
  SensorReadingEventArgs<CompassReading> e)
{
  if (_orientationCallback != null)
  {
    DispatcherHelper.CheckBeginInvokeOnUI(
      () => _orientationCallback(e.SensorReading.TrueHeading));
  }
}

Теперь нужно инициализировать DispatcherHelper. Для этого откройте App.xaml.cs и найдите метод InitializePhoneApplication. В самый конец этого метода добавьте выражение DispatcherHelper.Initialize();. Теперь запуск кода даст ожидаемый результат, корректно отображая направление устройства Windows Phone.

Заметьте, что в Windows Phone не все датчики генерируют свои события в фоновом потоке. Например, датчик GeoCoordinateWatcher, используемый для отслеживания геопозиционирования устройства, возвращает показания в основной поток для вашего удобства. Используя DispatcherHelper, вы не должны заботиться об этом и можете всегда единообразно инициировать обратный вызов основного потока.

Заключение

Мы обсудили, как Microsoft .NET Framework обрабатывает потоки и какие предосторожности нужно соблюдать, когда фоновому потоку нужно модифицировать некий объект, созданный основным потоком (также называемым UI-потоком). Вы убедились, что это способно привести к краху и, чтобы избежать такого краха, для корректной обработки операции следует использовать Dispatcher основного потока.

Потом я перешел к MVVM-приложению и ознакомил вас с компонентом DispatcherHelper из MVVM Light Toolkit. Я показал, как с помощью этого компонента можно избежать проблем взаимодействия из фонового потока с основным и как оптимизировать этот доступ и абстрагировать различия между WPF и другими инфраструктурами на основе XAML. Сделав это, можно совместно использовать код ViewModel и облегчить вашу работу.

Наконец, я продемонстрировал практический пример того, как DispatcherHelper можно использовать в приложении Windows Phone, чтобы избежать проблем при работе с некоторыми датчиками, которые генерируют свои события в фоновом потоке.

В следующей статье я детально рассмотрю компонент Messenger из MVVM Light и покажу, как он позволяет упростить взаимодействие между объектами без необходимости для них что-либо знать друг о друге.

Автор: Лёро Буньон  •  Иcточник: MSDN Magazine  •  Опубликована: 17.09.2014
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER
Теги:   MVVM.


Оценить статью:
Вверх
Комментарии посетителей RSS

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