Модель потоков
Приложение Windows Presentation Foundation (WPF) создано, чтобы помочь разработчикам избежать трудностей при разработке потоков. В результате, большинству разработчиков WPF не требуется писать интерфейс, использующий более одного потока. Поскольку многопоточные программы являются сложными и трудно отлаживаемыми, их следует избегать, если существуют однопоточные решения.
Независимо от качества архитектуры, никакая платформа UI не сможет предложить однопоточное решение для каждого типа задач. Приложение WPF достаточно близко приблизилось к решению этой проблемы, но по-прежнему есть ситуации, в которых несколько потоков улучшают быстродействие user interface (UI) или производительность приложения. После обсуждения некоторых основных материалов в данном документе рассматриваются подобные ситуации и в завершении обсуждаются некоторые более подробные сведения.
В этом разделе содержатся следующие подразделы.
- Общие сведения и Dispatcher
- Потоки в действии: примеры
- Технические подробности и важные моменты
- Связанные разделы
Общие сведения и Dispatcher
Разработка приложений WPF, как правило, начинается с двух потоков: одного для обработки визуализации, и другого управления UI. Поток визуализации эффективно выполняется незаметно для пользователя в фоновом режиме, тогда как поток UI получает входные данные, обрабатывает события, выводит изображение на экран и выполняет код приложения. Большинство приложений использует один поток UI, хотя в некоторых случаях лучше использовать несколько. Позже это будет рассмотрено на примере.
Поток UI ставит в очередь рабочие элементы внутри объекта, называемого Dispatcher. Объект Dispatcher выбирает рабочие элементы на основе приоритетов и выполняет каждый из них до завершения. Каждый поток UI должен иметь, по крайней мере, один объект Dispatcher, и каждый объект Dispatcher может выполнять рабочие элементы только в одном потоке.
Условием для построения быстро реагирующих, понятные пользователю приложений является максимальное повышение производительности Dispatcher, сохранения рабочие элементы небольшими. При таком методе элементы никогда не устаревают в очереди Dispatcher в ожидании обработки. Любая задержка между входными данными и ответами может разочаровывать пользователя.
Как в таком случае приложения WPF должны обрабатывать большие операции? Что если код включает большие вычисления или требуется запрос к базе данных на удаленном сервере? Обычно большие операции обрабатываются в отдельном потоке, оставляя поток UI свободным для обслуживания элементов в очереди Dispatcher. После завершения большой операции, она может передать результат обратно в поток UI для отображения.
Исторически сложилось так, что операционная система Windows позволяет получать доступ к элементам UI только создавшему их потоку. Это означает, что фоновый поток, отвечающий за некоторую длительную задачу, не может обновить текстовое поле при своем завершении. Windows создает это ограничение для обеспечения целостности компонентов UI. Список может выглядеть странно, если его содержимое обновляется фоновым потоком в процессе отображения.
WPF поддерживает встроенный механизм взаимного исключения, который осуществляет эту координацию. Большинство классов в приложении WPF являются производными от класса DispatcherObject. При создании DispatcherObject хранит ссылку на объект Dispatcher, связанный с текущим выполняемым потоком. По сути, DispatcherObject связывается с потоком, который его создал. Во время выполнения программы DispatcherObject может вызвать свой открытый метод VerifyAccess. VerifyAccess проверяет объект Dispatcher, связанный с текущим потоком, и сравнивает его со ссылкой на Dispatcher, которая сохраняется во время создания. Если они не совпадают, метод VerifyAccess вызывает исключение. VerifyAccess предназначен для вызова в начале каждого метода, принадлежащего объекту DispatcherObject.
Если только один поток может изменить UI, как фоновые потоки взаимодействуют с пользователем? Фоновый поток может попросить поток UI выполнить операцию от его имени. Он делает это путем регистрации рабочего элемента в объекте Dispatcher потока UI. Класс Dispatcher предоставляет два метода для регистрации рабочих элементов: Invoke и BeginInvoke. Оба метода назначают делегат для выполнения. Метод Invoke является синхронным вызовом — он не возвращает значение до тех пор, пока поток UI не закончит выполнение делегата. Метод BeginInvoke является асинхронным и немедленно возвращает значение.
Объект Dispatcher упорядочивает элементы в своей очереди по приоритету. Существуют десять уровней, которые могут быть указаны при добавлении элемента в очередь Dispatcher. Эти приоритеты сохраняются в перечислении DispatcherPriority. Подробные сведения об уровнях DispatcherPriority можно найти в Windows SDK документации.
Потоки в действии: примеры
Пример однопоточного приложения с длительным выполнением вычислений
Большинство graphical user interfaces (GUIs) тратят большую часть своего времени, простаивая в ожидании событий, которые создаются в ответ на действия пользователей. При внимательном программировании это время простоя можно использовать конструктивно, не понижая скорость отклика UI. Потоковая модель WPF не позволяет вводу прерывать операцию, которая происходит в потоке UI. Это означает, что периодически требуется возвращаться к объекту Dispatcher, чтобы обработать отложенные события ввода, прежде чем они станут устаревшими.
Рассмотрим следующий пример:
Это простое приложение ищет простые числа, начиная от трех и далее. При нажатии кнопки Пуск поиск начинается. Когда программа находит простое число, она обновляет пользовательский интерфейс. В любой точке пользователь может остановить поиск.
При всей простоте операции поиск простых чисел может происходить бесконечно, что представляет некоторые трудности. Если бы обработка всех операций поиска выполнялась внутри обработчика событий нажатия кнопки, поток UI никогда бы не получил возможность для обработки других событий. UI не мог бы ответить на входные данные или обработать сообщения. Он бы никогда не обновил отображение и не ответил бы на нажатие кнопки.
Можно провести поиск простого числа в отдельном потоке, но тогда пришлось бы иметь дело с проблемами синхронизации. С помощью однопотокового подхода можно непосредственно обновить подпись, в которой перечислено наибольшее простое число.
Если разбить задачу вычисления на управляемые фрагменты, можно периодически возвращаться к объекту Dispatcher и событиям обработки. Можно дать приложению WPF возможность обновлять и обрабатывать ввод.
Лучшим способом разбиения времени обработки между вычислением и обработкой события является управление вычислением из объекта Dispatcher. С помощью метода BeginInvoke можно запланировать проверку простого числа в той же очереди, из которой приходят события UI. В приведенном примере запланирована проверка только одного простого числа в каждый момент времени. После завершения проверки простого числа немедленно планируется следующая проверка. Эта проверка выполняется только после обработки ожидающих событий UI.
С помощью этого механизма приложение Microsoft Word выполняет проверку орфографии. Проверка орфографии выполняется в фоновом режиме, используя время простоя потока UI. Давайте посмотрим на код.
В следующем примере показан код XAML, который создает интерфейс пользователя.
<Window x:Class="SDKSamples.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Prime Numbers" Width="260" Height="75"
>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
<Button Content="Start"
Click="StartOrStop"
Name="startStopButton"
Margin="5,0,5,0"
/>
<TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
<TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
</StackPanel>
</Window>
<Window x:Class="SDKSamples.MainWindow"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Prime Numbers" Width="260" Height="75"
>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
<Button Content="Start"
Click="StartOrStop"
Name="startStopButton"
Margin="5,0,5,0"
/>
<TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
<TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
</StackPanel>
</Window>
Следующий пример демонстрирует фоновый код.
Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Threading
Imports System.Threading
Namespace SDKSamples
Partial Public Class MainWindow
Inherits Window
Public Delegate Sub NextPrimeDelegate()
'Current number to check
Private num As Long = 3
Private continueCalculating As Boolean = False
Public Sub New()
MyBase.New()
InitializeComponent()
End Sub
Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
If continueCalculating Then
continueCalculating = False
startStopButton.Content = "Resume"
Else
continueCalculating = True
startStopButton.Content = "Stop"
startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
End If
End Sub
Public Sub CheckNextNumber()
' Reset flag.
NotAPrime = False
For i As Long = 3 To Math.Sqrt(num)
If num Mod i = 0 Then
' Set not a prime flag to true.
NotAPrime = True
Exit For
End If
Next
' If a prime number.
If Not NotAPrime Then
bigPrime.Text = num.ToString()
End If
num += 2
If continueCalculating Then
startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
End If
End Sub
Private NotAPrime As Boolean = False
End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
{
public partial class Window1 : Window
{
public delegate void NextPrimeDelegate();
//Current number to check
private long num = 3;
private bool continueCalculating = false;
public Window1() : base()
{
InitializeComponent();
}
private void StartOrStop(object sender, EventArgs e)
{
if (continueCalculating)
{
continueCalculating = false;
startStopButton.Content = "Resume";
}
else
{
continueCalculating = true;
startStopButton.Content = "Stop";
startStopButton.Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
new NextPrimeDelegate(CheckNextNumber));
}
}
public void CheckNextNumber()
{
// Reset flag.
NotAPrime = false;
for (long i = 3; i <= Math.Sqrt(num); i++)
{
if (num % i == 0)
{
// Set not a prime flag to true.
NotAPrime = true;
break;
}
}
// If a prime number.
if (!NotAPrime)
{
bigPrime.Text = num.ToString();
}
num += 2;
if (continueCalculating)
{
startStopButton.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.SystemIdle,
new NextPrimeDelegate(this.CheckNextNumber));
}
}
private bool NotAPrime = false;
}
}
В следующем примере показан обработчик событий для Button.
Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
If continueCalculating Then
continueCalculating = False
startStopButton.Content = "Resume"
Else
continueCalculating = True
startStopButton.Content = "Stop"
startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
End If
End Sub
private void StartOrStop(object sender, EventArgs e)
{
if (continueCalculating)
{
continueCalculating = false;
startStopButton.Content = "Resume";
}
else
{
continueCalculating = true;
startStopButton.Content = "Stop";
startStopButton.Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
new NextPrimeDelegate(CheckNextNumber));
}
}
Помимо обновления текста в объекте Button этот обработчик отвечает за планирование проверки первого простого числа путем добавления делегата к очереди Dispatcher. Иногда после завершения работы этого обработчика событий объект Dispatcher выбирает этот делегат для выполнения.
Как упоминалось ранее, BeginInvoke является членом объекта Dispatcher, который используется при планировании делегата для выполнения. В этом случае, мы выбираем приоритет SystemIdle. Объект Dispatcher будет выполнять данный делегат только при отсутствии важных событий для обработки. Скорость ответа UI представляет большую важность, чем проверка числа. Также передается новый делегат, представляющий подпрограмму проверки числа.
Public Sub CheckNextNumber()
' Reset flag.
NotAPrime = False
For i As Long = 3 To Math.Sqrt(num)
If num Mod i = 0 Then
' Set not a prime flag to true.
NotAPrime = True
Exit For
End If
Next
' If a prime number.
If Not NotAPrime Then
bigPrime.Text = num.ToString()
End If
num += 2
If continueCalculating Then
startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
End If
End Sub
Private NotAPrime As Boolean = False
public void CheckNextNumber()
{
// Reset flag.
NotAPrime = false;
for (long i = 3; i <= Math.Sqrt(num); i++)
{
if (num % i == 0)
{
// Set not a prime flag to true.
NotAPrime = true;
break;
}
}
// If a prime number.
if (!NotAPrime)
{
bigPrime.Text = num.ToString();
}
num += 2;
if (continueCalculating)
{
startStopButton.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.SystemIdle,
new NextPrimeDelegate(this.CheckNextNumber));
}
}
private bool NotAPrime = false;
Этот метод проверяет, является ли следующее нечетное число простым. Если оно простое, метод непосредственно обновляет bigPrime TextBlock, чтобы отразить его результат поиска. Мы можем сделать так потому, что вычисление происходит в том же потоке, который был использован для создания компонента. Если использовать отдельный поток для вычислений, пришлось бы применить более сложный механизм синхронизации и выполнять обновления в потоке UI. Эта ситуация будет продемонстрирована далее.
Полный исходный код примера см. в разделе Single-Threaded Application with Long-Running Calculation Sample ("Пример однопоточного приложения с длительным выполнением вычислений").
Обработка блокирующей операции с фоновым потоком
Обработка блокировки операций в графическом приложении может оказаться трудной задачей. Мы не будем вызывать методы блокировки из обработчиков событий, так как приложение будет остановлено. Можно использовать отдельный поток для обработки этих операций, но затем нужно будет синхронизироваться с потоком UI, поскольку нельзя непосредственно изменить GUI из рабочего потока. Вставку делегатов в объект Dispatcher потока UI можно осуществить с помощью метода Invoke или BeginInvoke. В результате, эти делегаты будут выполнены с разрешением на изменение элементов UI.
В этом примере мы имитируем вызов удаленной процедуры, который получает прогноз погоды. Мы используем отдельный рабочий поток для выполнения этого вызова и планируем метод обновления в объекте Dispatcher потока UI при завершении.
Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Media
Imports System.Windows.Media.Animation
Imports System.Windows.Media.Imaging
Imports System.Windows.Shapes
Imports System.Windows.Threading
Imports System.Threading
Namespace SDKSamples
Partial Public Class Window1
Inherits Window
' Delegates to be used in placking jobs onto the Dispatcher.
Private Delegate Sub NoArgDelegate()
Private Delegate Sub OneArgDelegate(ByVal arg As String)
' Storyboards for the animations.
Private showClockFaceStoryboard As Storyboard
Private hideClockFaceStoryboard As Storyboard
Private showWeatherImageStoryboard As Storyboard
Private hideWeatherImageStoryboard As Storyboard
Public Sub New()
MyBase.New()
InitializeComponent()
End Sub
Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
' Load the storyboard resources.
showClockFaceStoryboard = CType(Me.Resources("ShowClockFaceStoryboard"), Storyboard)
hideClockFaceStoryboard = CType(Me.Resources("HideClockFaceStoryboard"), Storyboard)
showWeatherImageStoryboard = CType(Me.Resources("ShowWeatherImageStoryboard"), Storyboard)
hideWeatherImageStoryboard = CType(Me.Resources("HideWeatherImageStoryboard"), Storyboard)
End Sub
Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
' Change the status image and start the rotation animation.
fetchButton.IsEnabled = False
fetchButton.Content = "Contacting Server"
weatherText.Text = ""
hideWeatherImageStoryboard.Begin(Me)
' Start fetching the weather forecast asynchronously.
Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)
fetcher.BeginInvoke(Nothing, Nothing)
End Sub
Private Sub FetchWeatherFromServer()
' Simulate the delay from network access.
Thread.Sleep(4000)
' Tried and true method for weather forecasting - random numbers.
Dim rand As New Random()
Dim weather As String
If rand.Next(2) = 0 Then
weather = "rainy"
Else
weather = "sunny"
End If
' Schedule the update function in the UI thread.
tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
End Sub
Private Sub UpdateUserInterface(ByVal weather As String)
'Set the weather image
If weather = "sunny" Then
weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
ElseIf weather = "rainy" Then
weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
End If
'Stop clock animation
showClockFaceStoryboard.Stop(Me)
hideClockFaceStoryboard.Begin(Me)
'Update UI text
fetchButton.IsEnabled = True
fetchButton.Content = "Fetch Forecast"
weatherText.Text = weather
End Sub
Private Sub HideClockFaceStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
showWeatherImageStoryboard.Begin(Me)
End Sub
Private Sub HideWeatherImageStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
showClockFaceStoryboard.Begin(Me, True)
End Sub
End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
{
public partial class Window1 : Window
{
// Delegates to be used in placking jobs onto the Dispatcher.
private delegate void NoArgDelegate();
private delegate void OneArgDelegate(String arg);
// Storyboards for the animations.
private Storyboard showClockFaceStoryboard;
private Storyboard hideClockFaceStoryboard;
private Storyboard showWeatherImageStoryboard;
private Storyboard hideWeatherImageStoryboard;
public Window1(): base()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Load the storyboard resources.
showClockFaceStoryboard =
(Storyboard)this.Resources["ShowClockFaceStoryboard"];
hideClockFaceStoryboard =
(Storyboard)this.Resources["HideClockFaceStoryboard"];
showWeatherImageStoryboard =
(Storyboard)this.Resources["ShowWeatherImageStoryboard"];
hideWeatherImageStoryboard =
(Storyboard)this.Resources["HideWeatherImageStoryboard"];
}
private void ForecastButtonHandler(object sender, RoutedEventArgs e)
{
// Change the status image and start the rotation animation.
fetchButton.IsEnabled = false;
fetchButton.Content = "Contacting Server";
weatherText.Text = "";
hideWeatherImageStoryboard.Begin(this);
// Start fetching the weather forecast asynchronously.
NoArgDelegate fetcher = new NoArgDelegate(
this.FetchWeatherFromServer);
fetcher.BeginInvoke(null, null);
}
private void FetchWeatherFromServer()
{
// Simulate the delay from network access.
Thread.Sleep(4000);
// Tried and true method for weather forecasting - random numbers.
Random rand = new Random();
String weather;
if (rand.Next(2) == 0)
{
weather = "rainy";
}
else
{
weather = "sunny";
}
// Schedule the update function in the UI thread.
tomorrowsWeather.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.Normal,
new OneArgDelegate(UpdateUserInterface),
weather);
}
private void UpdateUserInterface(String weather)
{
//Set the weather image
if (weather == "sunny")
{
weatherIndicatorImage.Source = (ImageSource)this.Resources[
"SunnyImageSource"];
}
else if (weather == "rainy")
{
weatherIndicatorImage.Source = (ImageSource)this.Resources[
"RainingImageSource"];
}
//Stop clock animation
showClockFaceStoryboard.Stop(this);
hideClockFaceStoryboard.Begin(this);
//Update UI text
fetchButton.IsEnabled = true;
fetchButton.Content = "Fetch Forecast";
weatherText.Text = weather;
}
private void HideClockFaceStoryboard_Completed(object sender,
EventArgs args)
{
showWeatherImageStoryboard.Begin(this);
}
private void HideWeatherImageStoryboard_Completed(object sender,
EventArgs args)
{
showClockFaceStoryboard.Begin(this, true);
}
}
}
Ниже приведены некоторые подробности, на которые следует обратить внимание.
Создание обработчика кнопки
Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs) ' Change the status image and start the rotation animation. fetchButton.IsEnabled = False fetchButton.Content = "Contacting Server" weatherText.Text = "" hideWeatherImageStoryboard.Begin(Me) ' Start fetching the weather forecast asynchronously. Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer) fetcher.BeginInvoke(Nothing, Nothing) End Sub
private void ForecastButtonHandler(object sender, RoutedEventArgs e) { // Change the status image and start the rotation animation. fetchButton.IsEnabled = false; fetchButton.Content = "Contacting Server"; weatherText.Text = ""; hideWeatherImageStoryboard.Begin(this); // Start fetching the weather forecast asynchronously. NoArgDelegate fetcher = new NoArgDelegate( this.FetchWeatherFromServer); fetcher.BeginInvoke(null, null); }
При нажатии кнопки мы отображаем рисунок часов и запускаем анимацию. Мы отключаем кнопку. Мы вызываем метод FetchWeatherFromServer в новом потоке, а затем возвращаем, позволяя Dispatcher обрабатывать события во время ожидания сбора прогноза погоды.
Выборка погоды
Private Sub FetchWeatherFromServer() ' Simulate the delay from network access. Thread.Sleep(4000) ' Tried and true method for weather forecasting - random numbers. Dim rand As New Random() Dim weather As String If rand.Next(2) = 0 Then weather = "rainy" Else weather = "sunny" End If ' Schedule the update function in the UI thread. tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather) End Sub
private void FetchWeatherFromServer() { // Simulate the delay from network access. Thread.Sleep(4000); // Tried and true method for weather forecasting - random numbers. Random rand = new Random(); String weather; if (rand.Next(2) == 0) { weather = "rainy"; } else { weather = "sunny"; } // Schedule the update function in the UI thread. tomorrowsWeather.Dispatcher.BeginInvoke( System.Windows.Threading.DispatcherPriority.Normal, new OneArgDelegate(UpdateUserInterface), weather); }
Для просты мы фактически не используем никакого сетевого кода в данном примере. Вместо этого, мы моделируем задержку доступа к сети, задав для нашего нового потока спящий режим в течение четырех секунд. В течение этого времени исходный поток UI по-прежнему выполняется и отвечает на события. Чтобы показать это, была оставлена запущенная анимация, и кнопки свертывания и развертывания также продолжают работать.
После завершения задержки и случайного выбора прогноза погоды необходимо отправить сообщение обратно в поток UI. Для этого планируется вызов метода UpdateUserInterface в потоке UI с помощью объекта Dispatcher этого потока. В запланированный вызов этого метода передается строка, описывающая погоду.
Обновление UI
Private Sub UpdateUserInterface(ByVal weather As String) 'Set the weather image If weather = "sunny" Then weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource) ElseIf weather = "rainy" Then weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource) End If 'Stop clock animation showClockFaceStoryboard.Stop(Me) hideClockFaceStoryboard.Begin(Me) 'Update UI text fetchButton.IsEnabled = True fetchButton.Content = "Fetch Forecast" weatherText.Text = weather End Sub
private void UpdateUserInterface(String weather) { //Set the weather image if (weather == "sunny") { weatherIndicatorImage.Source = (ImageSource)this.Resources[ "SunnyImageSource"]; } else if (weather == "rainy") { weatherIndicatorImage.Source = (ImageSource)this.Resources[ "RainingImageSource"]; } //Stop clock animation showClockFaceStoryboard.Stop(this); hideClockFaceStoryboard.Begin(this); //Update UI text fetchButton.IsEnabled = true; fetchButton.Content = "Fetch Forecast"; weatherText.Text = weather; }
Если у объекта Dispatcher потока UI есть время, он выполняет запланированный вызов метода UpdateUserInterface. Этот метод останавливает анимацию часов и выбирает изображение для описания погоды. Он отображает это изображение и восстанавливает кнопку "Получить прогноз погоды".
Несколько окон, несколько потоков
Для некоторых приложений WPF требуется несколько окон верхнего уровня. Это полностью допустимо при управлении несколькими окнами с помощью комбинации одного потока и объекта Dispatcher, но иногда несколько потоков выполняют такую работу лучше. Это особенно верно, когда существует возможность, что одно из окон будет монополизировать поток.
Проводник Windows работает таким образом. Каждое новое окно проводника принадлежит исходному процессу, однако оно создается под управлением независимого потока.
Элемент управления Frame системы WPF позволяет отображать веб-страницы. Таким образом можно легко создать простую замену браузеру Internet Explorer. Начнем с важной функции: возможности открыть новое окно браузера. Когда пользователь нажимает кнопку "new window", запускается копия окна в отдельном потоке. Таким образом, долго выполняющиеся или блокирующие операции в одном из окон не блокируют все остальные окна.
На самом деле, браузера имеет свою собственную сложную поточную модель. Мы выбрали его, поскольку он знаком большинству читателей.
В следующем примере показан код.
<Window x:Class="SDKSamples.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="MultiBrowse"
Height="600"
Width="800"
Loaded="OnLoaded"
>
<StackPanel Name="Stack" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Button Content="New Window"
Click="NewWindowHandler" />
<TextBox Name="newLocation"
Width="500" />
<Button Content="GO!"
Click="Browse" />
</StackPanel>
<Frame Name="placeHolder"
Width="800"
Height="550"></Frame>
</StackPanel>
</Window>
Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Data
Imports System.Windows.Threading
Imports System.Threading
Namespace SDKSamples
Partial Public Class Window1
Inherits Window
Public Sub New()
MyBase.New()
InitializeComponent()
End Sub
Private Sub OnLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
placeHolder.Source = New Uri("https://www.msn.com")
End Sub
Private Sub Browse(ByVal sender As Object, ByVal e As RoutedEventArgs)
placeHolder.Source = New Uri(newLocation.Text)
End Sub
Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
newWindowThread.SetApartmentState(ApartmentState.STA)
newWindowThread.IsBackground = True
newWindowThread.Start()
End Sub
Private Sub ThreadStartingPoint()
Dim tempWindow As New Window1()
tempWindow.Show()
System.Windows.Threading.Dispatcher.Run()
End Sub
End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
{
public partial class Window1 : Window
{
public Window1() : base()
{
InitializeComponent();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
placeHolder.Source = new Uri("https://www.msn.com");
}
private void Browse(object sender, RoutedEventArgs e)
{
placeHolder.Source = new Uri(newLocation.Text);
}
private void NewWindowHandler(object sender, RoutedEventArgs e)
{
Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start();
}
private void ThreadStartingPoint()
{
Window1 tempWindow = new Window1();
tempWindow.Show();
System.Windows.Threading.Dispatcher.Run();
}
}
}
В данном контексте наиболее интересными являются следующие сегменты потоков этого кода:
Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
newWindowThread.SetApartmentState(ApartmentState.STA)
newWindowThread.IsBackground = True
newWindowThread.Start()
End Sub
private void NewWindowHandler(object sender, RoutedEventArgs e)
{
Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start();
}
Этот метод вызывается при нажатии кнопки "new window". Она создает новый поток и запускает его в асинхронном режиме.
Private Sub ThreadStartingPoint()
Dim tempWindow As New Window1()
tempWindow.Show()
System.Windows.Threading.Dispatcher.Run()
End Sub
private void ThreadStartingPoint()
{
Window1 tempWindow = new Window1();
tempWindow.Show();
System.Windows.Threading.Dispatcher.Run();
}
Этот метод является начальной точкой для нового потока. Мы создаем новое окно под элементом управления этого потока. Для управления новым потоком WPF автоматически создает новый диспетчер Dispatcher. Все что нужно сделать для обеспечения функциональности окна — это запустить диспетчер Dispatcher.
Технические подробности и важные моменты
Написание компонентов, использующих поток
В руководстве разработчика Microsoft .NET Framework описывается шаблон того, как компонент может предоставлять асинхронное поведение для своих клиентов (см. раздел Обзор асинхронной модели, основанной на событиях). Предположим, что нужно упаковать метод FetchWeatherFromServer в неграфический компонент многократного использования. Следуя стандартному шаблону Microsoft .NET Framework, это будет выглядеть примерно следующим образом.
Public Class WeatherComponent
Inherits Component
'gets weather: Synchronous
Public Function GetWeather() As String
Dim weather As String = ""
'predict the weather
Return weather
End Function
'get weather: Asynchronous
Public Sub GetWeatherAsync()
'get the weather
End Sub
Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
End Class
Public Class GetWeatherCompletedEventArgs
Inherits AsyncCompletedEventArgs
Public Sub New(ByVal [error] As Exception, ByVal canceled As Boolean, ByVal userState As Object, ByVal weather As String)
MyBase.New([error], canceled, userState)
_weather = weather
End Sub
Public ReadOnly Property Weather() As String
Get
Return _weather
End Get
End Property
Private _weather As String
End Class
Public Delegate Sub GetWeatherCompletedEventHandler(ByVal sender As Object, ByVal e As GetWeatherCompletedEventArgs)
public class WeatherComponent : Component
{
//gets weather: Synchronous
public string GetWeather()
{
string weather = "";
//predict the weather
return weather;
}
//get weather: Asynchronous
public void GetWeatherAsync()
{
//get the weather
}
public event GetWeatherCompletedEventHandler GetWeatherCompleted;
}
public class GetWeatherCompletedEventArgs : AsyncCompletedEventArgs
{
public GetWeatherCompletedEventArgs(Exception error, bool canceled,
object userState, string weather)
:
base(error, canceled, userState)
{
_weather = weather;
}
public string Weather
{
get { return _weather; }
}
private string _weather;
}
public delegate void GetWeatherCompletedEventHandler(object sender,
GetWeatherCompletedEventArgs e);
GetWeatherAsync использовал бы один из методов, описанных выше, таких как создание фонового потока, для работы в асинхронном режиме, не блокируя вызов потока.
Одной из наиболее важных частей этого шаблона является вызов метода MethodNameCompleted в том же потоке, который вызвал метод MethodNameAsync. Это можно сделать с помощью WPF довольно просто, сохранения CurrentDispatcher, но затем неграфический компонент может использоваться только в приложениях WPF, а не в программах Windows Forms или ASP.NET.
Класс DispatcherSynchronizationContext создан для решения этой задачи — представляйте его упрощенную версию Dispatcher, который работает также другими средами UI.
Public Class WeatherComponent2
Inherits Component
Public Function GetWeather() As String
Return fetchWeatherFromServer()
End Function
Private requestingContext As DispatcherSynchronizationContext = Nothing
Public Sub GetWeatherAsync()
If requestingContext IsNot Nothing Then
Throw New InvalidOperationException("This component can only handle 1 async request at a time")
End If
requestingContext = CType(DispatcherSynchronizationContext.Current, DispatcherSynchronizationContext)
Dim fetcher As New NoArgDelegate(AddressOf Me.fetchWeatherFromServer)
' Launch thread
fetcher.BeginInvoke(Nothing, Nothing)
End Sub
Private Sub [RaiseEvent](ByVal e As GetWeatherCompletedEventArgs)
RaiseEvent GetWeatherCompleted(Me, e)
End Sub
Private Function fetchWeatherFromServer() As String
' do stuff
Dim weather As String = ""
Dim e As New GetWeatherCompletedEventArgs(Nothing, False, Nothing, weather)
Dim callback As New SendOrPostCallback(AddressOf DoEvent)
requestingContext.Post(callback, e)
requestingContext = Nothing
Return e.Weather
End Function
Private Sub DoEvent(ByVal e As Object)
'do stuff
End Sub
Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
Public Delegate Function NoArgDelegate() As String
End Class
public class WeatherComponent2 : Component
{
public string GetWeather()
{
return fetchWeatherFromServer();
}
private DispatcherSynchronizationContext requestingContext = null;
public void GetWeatherAsync()
{
if (requestingContext != null)
throw new InvalidOperationException("This component can only handle 1 async request at a time");
requestingContext = (DispatcherSynchronizationContext)DispatcherSynchronizationContext.Current;
NoArgDelegate fetcher = new NoArgDelegate(this.fetchWeatherFromServer);
// Launch thread
fetcher.BeginInvoke(null, null);
}
private void RaiseEvent(GetWeatherCompletedEventArgs e)
{
if (GetWeatherCompleted != null)
GetWeatherCompleted(this, e);
}
private string fetchWeatherFromServer()
{
// do stuff
string weather = "";
GetWeatherCompletedEventArgs e =
new GetWeatherCompletedEventArgs(null, false, null, weather);
SendOrPostCallback callback = new SendOrPostCallback(DoEvent);
requestingContext.Post(callback, e);
requestingContext = null;
return e.Weather;
}
private void DoEvent(object e)
{
//do stuff
}
public event GetWeatherCompletedEventHandler GetWeatherCompleted;
public delegate string NoArgDelegate();
}
Вложенная накачка
Иногда невозможно полностью заблокировать поток UI. Рассмотрим метод Show класса MessageBox. Show возвращает значение лишь после того, как пользователь нажмет кнопку "ОК". Однако он создает окно, которое должно иметь цикл обработки сообщений, чтобы быть интерактивным. Ожидая, когда пользователь нажмет кнопку "ОК", исходное окно приложения не отвечает на ввод данных пользователем. Тем не менее оно продолжает обрабатывать сообщения отображения. Исходное окно перерисовывается при его перекрытии и выведении.
Данное окно сообщения должно подчиняться какому-либо потоку. Приложение WPF могло бы создать новый поток специально для данного окна сообщения, но этот поток не смог бы отображать отключенные элементы в исходном окне (вспомните предыдущее обсуждение взаимного исключения). Вместо этого, WPF использует систему обработки вложенных сообщений. Класс Dispatcher содержит специальный метод PushFrame, который сохраняет текущую точку выполнения приложения и затем начинает новый цикл обработки сообщений. После завершения цикла обработки вложенных сообщений выполнение возобновляется после вызова исходного метода PushFrame.
В этом случае, PushFrame поддерживает программный контекст при вызове MessageBox. Show начинает новый цикл обработки сообщений для перерисовки фона окна и обработки входных данных для окна сообщения. Когда пользователь нажимает кнопку "ОК" и очищает всплывающее окно, вложенные циклы завершаются и управление возобновляется после вызова Show.
Устаревшие маршрутизированные события
Маршрутизация системы обработки событий в приложении WPF уведомляет все деревья, когда вызываются события.
<Canvas MouseLeftButtonDown="handler1"
Width="100"
Height="100"
>
<Ellipse Width="50"
Height="50"
Fill="Blue"
Canvas.Left="30"
Canvas.Top="50"
MouseLeftButtonDown="handler2"
/>
</Canvas>
При нажатии левой кнопки эллипса выполняется handler2. После завершения работы handler2 событие передается объекту Canvas, который использует handler1 для его обработки. Это происходит, только если handler2 явно не помечает объект события как обработанный.
Возможно, обработка этого события займет у handler2 много времени. Для запуска цикла вложенных сообщений, который не будет возвращаться в течение нескольких часов, handler2 может использовать PushFrame. Если handler2 не помечает события как обработанное после завершения цикла обработки сообщений, событие передается вверх по дереву, даже если оно является очень старым.
Повторный вход и блокировка
Механизм блокировки common language runtime (CLR) не работает в точности так, как это можно представить; можно было бы ожидать, что, запрашивая блокировку, поток полностью завершает операцию. В действительности, поток продолжает получать и обрабатывать сообщения с высоким приоритетом. Это помогает избежать взаимоблокировок и максимально повышает скорость отклика интерфейсов, но может приводить к незначительным ошибкам. В большинстве случаев нет необходимости что-либо знать об этом, но иногда (как правило, в ситуациях, включающих сообщения окон Win32 или компоненты COM STA) знания могут потребоваться.
Большинство интерфейсов построено без учета безопасности потоков, так как разработчик предполагает, что доступ к UI всегда выполняется не более чем одним потоком. В этом случае, предполагается, что неблагоприятные последствия, вносимые одним потоком при изменении среды в непредвиденное время, устраняет механизм взаимного исключения DispatcherObject. Рассмотрим следующий псевдокод:
Большую часть времени все работает правильно, но в какой-то момент непредвиденный повторный вход может действительно вызвать проблемы в приложении WPF. Поэтому в некий ключевой момент WPF вызывает метод DisableProcessing, который меняет инструкцию блокировки для этого потока, чтобы использовать свободную от повторного входа блокировку WPF вместо обычной блокировки CLR.
Так почему же команда CLR выбрала такое поведение? Это было связано с объектами COM STA и завершением потока. Если объект удаляется сборщиком мусора, его метод Finalize выполняется не в потоке UI, а в выделенном потоке метода завершения. Именно здесь лежит проблема, поскольку объект COM STA, созданный в потоке UI, может быть удален только в потоке UI. Среда CLR предоставляет эквивалент метода BeginInvoke (в данном случае с помощью метода SendMessage Win32). Однако если поток UI занят, поток метода завершения устаревает и объект COM STA не удается завершить, что приводит к серьезной утечке памяти. Поэтому команда CLR создала сложный вызов для формирования такого механизма блокировки.
Задача приложения WPF — избежать непредвиденного повторного входа без внесения "утечки памяти", вот почему мы не блокируем где-либо повторный вход.
См. также
Другие ресурсы
Single-Threaded Application with Long-Running Calculation Sample