Wpf dispatcher что это
Класс DispatcherObject
Большую часть времени вы не будете взаимодействовать с диспетчером напрямую. Однако немало времени придется тратить на использование экземпляров DispatcherObject, потому что каждый визуальный объект WPF наследуется от этого класса. DispatcherObject — это просто объект, привязанный к диспетчеру. Другими словами — объект, привязанный к потоку диспетчера.
DispatcherObject имеет всего три члена, которые перечислены в таблице:
Имя | Описание |
---|---|
Dispather | Возвращает диспетчер, управляющий данным объектом. |
CheckAccess() | Возвращает true, если код находится в правильном потоке для использования объекта; в противном случае возвращает false. |
VerifyAccess() | Ничего не делает, если код находится в правильном потоке для использования объекта; в противном случае генерирует исключение InvalidOperationException. |
Объекты WPF часто вызывают VerifyAccess(), чтобы защитить себя. Они не вызывают VerifyAccess() в ответ на каждую операцию (поскольку это было бы слишком накладно по производительности), но вызывают этот метод достаточно часто, чтобы было маловероятным долго использовать объект из неверного потока.
Например, следующий код реагирует на щелчок на кнопке, создавая новый объект System.Threading.Thread. Затем он использует этот поток для вызова небольшого фрагмента кода, который изменяет текстовое поле в текущем окне:
Этот код специально задуман так, чтобы выдать сбой. Метод UpdateTextWrong() будет выполнен в новом потоке, которому не разрешен доступ к объектам WPF. В этом случае объект TextBox перехватывает нарушение, вызывая VerifyAccess(), при этом генерируется исключение InvalidOperationException.
Чтобы исправить код, понадобится получить ссылку на диспетчер, владеющий объектом TextBox (тот же самый диспетчер, который владеет окном и всеми прочими объектами WPF в приложении). Получив доступ к этому диспетчеру, можно вызывать Dispatcher.BeginInvoke(), чтобы маршализировать некоторый код потоку диспетчера. По сути, BeginInvoke() планирует указанный код в качестве задачи для диспетчера. Затем диспетчер выполняет этот код. Ниже показан корректный код:
Метод Dispatcher.BeginInvoke() принимает два параметра. Первый указывает свойство задачи. В большинстве случаев будет применяться DispatcherPriority.Normal, но можно также использовать более низкий приоритет, если есть задача, которая не обязательно должна быть завершена немедленно, и которую можно отложить до того момента, когда диспетчеру нечего будет делать.
Например, это может иметь смысл, если нужно отобразить сообщение о состоянии длительно выполняющейся операции где-то в рамках пользовательского интерфейса. Можно использовать DispatcherPriority.ApplicationIdle, чтобы подождать, пока приложение завершит всю прочую работу, либо еще более «сдержанный» метод DispatcherPriority.SystemIdle, чтобы подождать, пока вся система не придет в состояние ожидания, и центральный процессор не станет простаивать.
Допускается также применять пониженный приоритет, чтобы отвлечь внимание диспетчера на что-то другое. Однако рекомендуется оставлять высокие приоритеты для событий ввода (таких как нажатия клавиш). Они должны обрабатываться почти постоянно, или же возникнет впечатление, что приложение несколько медлительно. С другой стороны, добавление нескольких миллисекунд ко времени выполнения фоновой операции не будет заметно, так что приоритет DispatcherPriority.Normal более оправдан в такой ситуации.
Второй параметр BeginInvoke() — это делегат, указывающий на метод с кодом, который необходимо выполнить. Этот метод может находиться где-то в другом месте кода, или же его можно определить встроенным (как в приведенном примере). Подход на основе встроенного кода хорош для простых операций, таких как обновление в одной строке. Однако если нужно использовать более сложный процесс для обновления пользовательского интерфейса, лучше будет вынести такой код в отдельный метод.
Метод BeginInvoke() также возвращает значение, которое в данном примере не используется. BeginInvoke() возвращает объект DispatcherOperation, который позволяет получить состояние операции маршализации и определить, когда код действительно был выполнен. Однако DispatcherOperation применяется редко, потому что код, который передается BeginInvoke(), должен выполняться за очень короткое время.
Помните, что длительная фоновая операция должна выполняться в отдельном потоке, а результат маршализироваться потоку диспетчера (и в этот момент будет обновлен пользовательский интерфейс, чтобы изменить разделяемый объект). Не имеет смысла выполнять длительно работающий код в методе, который передается BeginInvoke(). Например, приведенный ниже слегка реорганизованный код работает, однако он менее практичен:
Здесь проблема заключается в том, что вся работа происходит в потоке диспетчера. Это значит, что код займет диспетчер примерно так же, как это происходило бы в приложении без многопоточности.
Диспетчер также предоставляет метод Invoke(). Подобно BeginInvoke(), он маршализирует указанный код потоку диспетчера. Но в отличие от BeginInvoke(), метод Invoke() останавливает поток до тех пор, пока диспетчер выполняет код. Метод Invoke() можно использовать, если нужно приостановить асинхронную операцию до тех пор, пока от пользователя не поступит какой-нибудь отклик.
Например, метод Invoke() можно вызвать для запуска фрагмента кода, отображающего диалоговое окно с кнопками ОК и Cancel. После того как пользователь щелкнет на кнопке и маршализируемый код завершится, Invoke() вернет управление, и можно будет продолжить работу в соответствии с ответом пользователя.
Before we learn what is dispatcher and why we need it, we need to understand what is the apartments of the Thread.
Thread Apartments
All objects in the process are grouped into Apartments.
There are two types of apartments in Threads:
Single-Threaded Apartment (STA)
Single-threaded apartments contains only one thread. All objects in this apartment can receive method calls from only this thread. Objects does not need synchronization because all methods calls are comes synchronously from single thread.
Single-threaded apartment needs a message queue to handle calls from other threads. When other threads calls an object in STA thread then the method call are queued in the message queue and STA object will receive a call from that message queue.
Multi-Threaded Apartment (MTA)
Multi-threaded apartments contains one or more threads. All objects in this apartment can receive calls from any thread. All objects are self responsible for maintaining the synchronization of their data.
WPF Dispatcher
A WPF application must start in single-threaded apartment thread. STA have a message queue to synchronize method calls within his apartment. As well as other threads outside the apartment can’t update the objects directly. They must place their method call into the message queue to update the objects in STA.
Dispatcher owns the message queue for the STA thread.
When you execute a WPF application, it automatically create a new Dispatcher object and calls its Run method. Run method is used for initializing the message queue.
When WPF application starts, it creates two threads:
UI thread is responsible all the user inputs, handle events, paints screen and run the application code. Render threads runs in the background and used for render the WPF screen.
WPF Dispatcher is associated with the UI thread. The UI thread queues methods call inside the Dispatcher object. Whenever your changes the screen or any event executes, or call a method in the code-behind all this happen in the UI thread and UI thread queue the called method into the Dispatcher queue. Dispatcher execute its message queue into the synchronous order.
How all WPF objects refers to single Dispatcher?
Every WPF control whether it is Window, button or textbox inherits from DispatcherObject. Below is class hierarchy diagram.
When WPF creates an instance of Button, it calls the protected constructor of DispatcherObject. DispatcherObject contains a property of type Dispatcher. In the constructor, it save the reference of current thread Dispatcher to Dispatcher property of DispatcherObject.
Why we need Dispatcher?
WPF works with Dispatcher object behind the scenes and we don’t need to work with Dispatcher when we are working on the UI thread.
When we create a new thread for offloading the work and want to update the UI from the other thread then we must need Dispatcher. Only Dispatcher can update the objects in the UI from non-UI thread.
Dispatcher provides two methods for registering method to execute into the message queue.
Invoke
Invoke method takes an Action or Delegate and execute the method synchronously. That means it does not return until the Dispatcher complete the execution of the method.
Here is an example of Invoke:
Above code will create a new thread using Task.Factory and immediately start the thread. In the InvokeMethodExample if we try to directly call to update the Content property of btn1 object. It will throws a System.InvalidOperationException. We have used Invoke method of Dispatcher. In the Invoke method, I pass the Action and update the Content property of Button object. It will not throws any error and successfully update the Content property.
BeginInvoke
BeginInvoke method take a Delegate but it executes the method asynchronously. That means it immediately returns before calling the method.
BeginInvoke returns a DispatcherOperation object. This object can be used for knowing the status of operation whether it is completed or not. It also provides two event Aborted and Completed.
Модель STA
Как известно платформа WPF коренным образом изменила почти все основы программирования для Windows. Она предложила новый подход ко всему — от определения содержимого окна до отображения трехмерной графики. Платформа WPF даже представила новые концепции, которые не являются очевидным образом сосредоточенными на пользовательском интерфейсе, такие как свойства зависимости и маршрутизируемые события.
Многопоточность
Многопоточность — это искусство выполнения более чем одного фрагмента кода одновременно. Целью многопоточности обычно является создание более отзывчивого интерфейса — такого, который не «замораживается» во время работы, — хотя многопоточность можно применять также и для того, чтобы полнее задействовать преимущества двухядерного процессора при выполнении ресурсоемких алгоритмов или другой работы одновременно с некоторой длительной операцией (например, чтобы выполнять некоторые вычисления в процессе ожидания ответа от веб-службы).
Элементы WPF обладают потоковой родственностью (thread affinity). Поток, который создает их, владеет ими, и другие потоки не могут взаимодействовать с ними напрямую. (Элемент — это объект WPF, отображаемый в окне.)
Объекты WPF, обладающие потоковой родственностью, наследуются от DispatcherObject в некоторой точке их иерархии классов. DispatcherObject включает небольшой набор членов, которые позволяют верифицировать, выполняется ли код в правильном потоке, чтобы использовать определенный объект, и (если нет) переключаться на другой поток.
На практике один поток выполняет все приложение и владеет объектами WPF. Хотя можно использовать отдельные потоки, чтобы отображать отдельные окна, такое проектное решение встречается редко.
Диспетчер
Диспетчер управляет работой, происходящей в WPF-приложении. Диспетчер владеет потоком приложения и управляет очередью элементов работы. Во время работы приложения диспетчер принимает новые запросы работы и выполняет их по одному.
Формально диспетчер создается при первоначальном создании в новом потоке экземпляра класса, который наследуется от DispatcherObject. В случае создания отдельных потоков и использования их для отображения отдельных окон получается более одного диспетчера. Однако большинство приложений не усложняют картину и обходятся одним потоком пользовательского интерфейса и одним диспетчером. Затем они используют многопоточность для управления операциями с данными и другими фоновыми задачами.
Многопоточность и диспетчеризация в MVVM-приложениях
Продукты и технологии:
Windows 8, MVVM Light Toolkit, Visual Studio, Windows Phone, Windows Presentation Foundation
В статье рассматриваются:
Примерно год назад я начал серию статей по шаблону 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 считывается файл (синхронно):
А это эквивалентная асинхронная операция в Windows 8:
Обратите внимание на присутствие ключевых слов await и async в версии для Windows 8. Они предназначены для того, чтобы избегать использования обратных вызовов в асинхронных операциях и чтобы код было легче читать. Здесь они нужны, поскольку файловая операция асинхронная. В WPF-версии, напротив, эта операция синхронная, что создает риск блокировки основного потока, если считываемый файл окажется длинным. Это может вызвать сделать анимации прерывистыми или привести к тому, что UI перестанет обновляться, т. е. ухудшить UX.
Аналогично длительные операции в вашем приложении должны выноситься в фоновые потоки, если есть риск того, что они будут мешать обновлению UI. Например, в WPF, Silverlight и Windows Phone код на рис. 1 инициирует фоновую операцию, которая выполняется в длительном цикле. В каждом цикле поток ненадолго переводится в спящее состояние, чтобы дать время другим потокам обработать свои операции.
Взаимодействие потоков
Когда одному из потоков нужно взаимодействовать с другим, следует соблюдать некоторые меры предосторожности. Например, я модифицирую код с рис. 1 так, чтобы он отображал сообщение о состоянии в каждом цикле. Для этого я просто добавлю строку кода в цикл while, которая устанавливает свойство Text элемента управления StatusTextBlock, находящегося в XAML:
Приложение SimpleMultiThreading в коде, сопутствующем этой статье, демонстрирует этот пример. Если вы запускаете его, используя кнопку Start (crashes the app), то в приложении действительно происходит крах. Что же случилось? Когда объект создается, его владельцем становится тот поток, в котором был вызван метод конструктора. В случае UI-элементов объекты создаются анализатором XAML (XAML parser) при загрузке XAML-документа. Все это происходит в основном потоке. В итоге все UI-элементы принадлежат основному потоку, который также зачастую называют UI-потоком. Когда фоновый поток в предыдущем коде пытается модифицировать свойство Text элемента StatusTextBlock, обнаруживается недопустимое обращение между потоками. Как следствие, генерируется исключение. Это можно увидеть, выполняя данный код в отладчике. На рис. 2 показан диалог исключения. Обратите внимание на сообщение «Additional information», которое указывает корень проблемы.
Рис. 2. Диалог исключения, вызванного недопустимым обращением из одного потока в другой
Рис. 3. Иерархия классов
Рис. 4. Диспетчеризация вызова UI-потоку
Диспетчеризация в MVVM-приложениях
Когда фоновая операция запускается из ViewModel, ситуация несколько меняется. Обычно ViewModel не наследует от DispatcherObject. ViewModel — это POCO-объекты (Plain Old CLR Objects), которые реализуют интерфейс INotifyPropertyChanged. Например, на рис. 5 показан ViewModel, производный от класса ViewModelBase из MVVM Light. В истинном стиле MVVM я добавляю наблюдаемое свойство Status, генерирующее событие PropertyChanged. Затем из кода фонового потока я пытаюсь записать в это свойство информационное сообщение.
Рис. 5. Обновление связанного свойства в ViewModel
Если запустить этот код в 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 для предотвращения краха
Как и предполагает название, этот метод сначала выполняет проверку. Если код, вызвавший этот метод, уже выполняется в основном потоке, никакой диспетчеризации не требуется. В таком случае делегат выполняется немедленно — прямо в основном потоке. Однако, если код, вызвавший этот метод, находится в фоновом потоке, происходит диспетчеризация.
Поскольку метод выполняет проверку перед диспетчеризацией, вызывающий может быть уверен, что код всегда будет использовать оптимальный вызов. Это особенно полезно, когда вы пишете кросс-платформенный код, где многопоточность может работать с небольшими различиями на разных платформах. В данном случае код 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 замените конструктор по умолчанию следующим кодом:
К сожалению, в эмуляторе Windows Phone нет возможности сымитировать аппаратный компас. Для проверки кода придется запустить приложение на физическом устройстве. Подключите это устройство и запустите код в режиме отладки, нажав F5. Наблюдайте за консольным окном Output в Visual Studio. Вы увидите вывод Compass. Если вы подвигаете устройство, то сможете найти север и наблюдать, как будет постоянно обновляться значение.
Затем я свяжу TextBlock в XAML со свойством Heading в MainViewModel. Откройте MainPage.xaml и найдите TextBlock в ContentPanel. Замените «Nothing yet» в свойстве Text на «
Ошибка возникает потому, что датчик компаса обрабатывается фоновым потоком. Когда запускается обратный вызов, он также выполняется в фоновом потоке, как и аксессор set свойства Heading. Поскольку TextBlock принадлежит основному потоку, генерируется исключение. Здесь тоже нужно создать «безопасную зону» — позаботиться о диспетчеризации операций в основной поток. Для этого откройте класс SensorService. Событие CurrentValueChanged обрабатывается методом CompassCurrentValueChanged; как раз в нем выполняется метод обратного вызова. Замените этот код показанным ниже, где используется DispatcherHelper:
Теперь нужно инициализировать DispatcherHelper. Для этого откройте App.xaml.cs и найдите метод InitializePhoneApplication. В самый конец этого метода добавьте выражение DispatcherHelper.Initialize();. Теперь запуск кода даст ожидаемый результат, корректно отображая направление устройства Windows Phone.
Заметьте, что в Windows Phone не все датчики генерируют свои события в фоновом потоке. Например, датчик GeoCoordinateWatcher, используемый для отслеживания геопозиционирования устройства, возвращает показания в основной поток для вашего удобства. Используя DispatcherHelper, вы не должны заботиться об этом и можете всегда единообразно инициировать обратный вызов основного потока.
Заключение
Потом я перешел к MVVM-приложению и ознакомил вас с компонентом DispatcherHelper из MVVM Light Toolkit. Я показал, как с помощью этого компонента можно избежать проблем взаимодействия из фонового потока с основным и как оптимизировать этот доступ и абстрагировать различия между WPF и другими инфраструктурами на основе XAML. Сделав это, можно совместно использовать код ViewModel и облегчить вашу работу.
Наконец, я продемонстрировал практический пример того, как DispatcherHelper можно использовать в приложении Windows Phone, чтобы избежать проблем при работе с некоторыми датчиками, которые генерируют свои события в фоновом потоке.
В следующей статье я детально рассмотрю компонент Messenger из MVVM Light и покажу, как он позволяет упростить взаимодействие между объектами без необходимости для них что-либо знать друг о друге.
Лёро Буньон (Laurent Bugnion) — старший директор IdentityMine Inc., партнера Microsoft, работающего с такими технологиями, как Windows Presentation Foundation, Silverlight, Pixelsense, Kinect, Windows 8, Windows Phone и UX. Живет в Цюрихе (Швейцария). Также является обладателем званий Microsoft MVP и Microsoft Regional Director.
Выражаю благодарность за рецензирование статьи эксперту Microsoft Томасу Петчелу (Thomas Petchel).