Модель потоков
Windows Presentation Foundation (WPF) предназначена для экономии разработчиков от трудностей потоков. В результате большинство разработчиков WPF не записывают интерфейс, использующий несколько потоков. Поскольку многопотоковые программы являются сложными и трудно отлаживаемыми, их следует избегать, если существуют однопоточные решения.
Независимо от того, насколько хорошо архитектор, однако, ни одна платформа пользовательского интерфейса не может предоставлять однопоточное решение для каждой проблемы. Приложения WPF достаточно близко приблизились к решению этой проблемы, но по-прежнему встречаются ситуации, когда использование нескольких потоков улучшает быстродействие пользовательского интерфейса или производительность приложения. После обсуждения некоторых фоновых материалов в этой статье рассматриваются некоторые из этих ситуаций, а затем завершается обсуждением некоторых более низких уровней подробностей.
Примечание.
В данном разделе обсуждается работа с потоками и использование метода InvokeAsync для асинхронных вызовов. Метод InvokeAsync
принимает или Func<TResult> в Action качестве параметра и возвращает или возвращает DispatcherOperation или DispatcherOperation<TResult>имеет Task свойство. Ключевое слово await
можно использовать как для DispatcherOperation, так и для связанной задачи Task. Если необходимо синхронное ожидание объекта Task, возвращаемого DispatcherOperation или DispatcherOperation<TResult>, вызывайте метод расширения DispatcherOperationWait. Вызов Task.Wait приведет к взаимоблокировке. Дополнительные сведения об использовании Task для выполнения асинхронных операций см. в статье Асинхронное программирование на основе задач.
Чтобы сделать синхронный вызов, используйте Invoke метод, который также имеет перегрузки, принимающее делегат или ActionFunc<TResult> параметр.
Обзор и диспетчер
Как правило, приложения WPF начинаются с двух потоков: один для обработки отрисовки и другого для управления пользовательским интерфейсом. Поток визуализации эффективно выполняется незаметно для пользователя в фоновом режиме, тогда как поток пользовательского интерфейса получает входные данные, обрабатывает события, выводит изображение на экран и выполняет код приложения. Большинство приложений использует один поток пользовательского интерфейса, хотя в некоторых случаях лучше использовать несколько. Мы обсудим это с примером позже.
Поток пользовательского интерфейса создает очередь рабочих элементов внутри объекта с именем Dispatcher. Объект Dispatcher выбирает рабочие элементы на основе приоритетов и выполняет каждый из них до завершения. Каждый поток пользовательского интерфейса должен иметь хотя бы один объект Dispatcher, и каждый из объектов Dispatcher может выполнять рабочие элементы только в одном потоке.
Условием для построения быстро реагирующих, понятных пользователю приложений является максимальное повышение производительности Dispatcher путем сохранения небольших размеров рабочих элементов. При таком подходе элементы никогда не устаревают в очереди Dispatcher в ожидании обработки. Любая задержка между входными данными и ответами может разочаровать пользователя.
Как тогда приложения WPF должны обрабатывать большие операции? Что если код включает большие вычисления или требуется запрос к базе данных на удаленном сервере? Как правило, ответ заключается в обработке большой операции в отдельном потоке, оставляя поток пользовательского интерфейса свободным от элементов в Dispatcher очереди. После завершения большой операции она может передать результат обратно в поток пользовательского интерфейса для отображения.
Исторически сложилось так, что операционная система Windows позволяет получать доступ к элементам пользовательского интерфейса только создавшему их потоку. Это означает, что фоновый поток, отвечающий за некоторую длительную задачу, не может обновить текстовое поле при своем завершении. Windows создает это ограничение для обеспечения целостности компонентов. Список может выглядеть странно, если его содержимое обновляется фоновым потоком в процессе отображения.
Платформа WPF имеет встроенный механизм взаимного исключения, который осуществляет такую координацию. Большинство классов в WPF являются производными от DispatcherObject. В конструкторе DispatcherObject сохраняет ссылку на объект Dispatcher, привязанный к выполняемому в настоящий момент потоку. Как результат, DispatcherObject ассоциируется с потоком, который создал его. Во время выполнения программы DispatcherObject может вызвать общедоступный метод VerifyAccess. VerifyAccess проверяет объект Dispatcher, связанный с текущим потоком и сравнивает его с ссылкой Dispatcher, сохраненной в конструкторе. Если они не соответствуют, VerifyAccess вызывает исключение. VerifyAccess следует вызывать вначале любого метода, принадлежащего DispatcherObject.
Если только один поток может изменить пользовательский интерфейс, как же фоновые потоки взаимодействуют с пользователем? Фоновый поток может попросить поток пользовательского интерфейса выполнить операцию от его имени. Он делает это путем регистрации рабочего элемента в объекте Dispatcher потока пользовательского интерфейса. Класс Dispatcher предоставляет методы для регистрации рабочих элементов: Dispatcher.InvokeAsync, Dispatcher.BeginInvokeи Dispatcher.Invoke. Эти методы запланируйте делегат для выполнения. Invoke
является синхронным вызовом, то есть он не возвращается, пока поток пользовательского интерфейса фактически завершит выполнение делегата. InvokeAsync
и BeginInvoke
являются асинхронными и возвращаются немедленно.
Dispatcher чередует элементы в очереди в порядке приоритета. Существуют десять уровней, которые могут быть указаны при добавлении элемента в очередь Dispatcher. Приоритет задается в перечислении DispatcherPriority.
Однопоточное приложение с длительным вычислением
Большинство графических пользовательских интерфейсов (GUI) тратят внушительную часть своего времени, простаивая в ожидании событий, которые создаются в ответ на действия пользователей. При внимательном программировании это время простоя можно использовать конструктивно, не понижая скорость отклика пользовательского интерфейса. Модель потоков WPF не позволяет входным данным прерывать операцию, которая происходит в потоке пользовательского интерфейса. Это означает, что периодически требуется возвращаться к объекту Dispatcher, чтобы обработать отложенные события ввода, прежде чем они станут устаревшими.
Пример приложения, демонстрирующего основные понятия этого раздела, можно скачать с GitHub для C# или Visual Basic.
Рассмотрим следующий пример:
Это простое приложение ищет простые числа, начиная от трех и далее. При нажатии пользователем кнопки Start начинается поиск. Когда программа находит простое число, она обновляет пользовательский интерфейс. В любой момент пользователь может остановить поиск.
При всей простоте операции поиск простых чисел может происходить бесконечно, что представляет некоторые трудности. Если бы обработка всех операций поиска выполнялась внутри обработчика событий нажатия кнопки, поток пользовательского интерфейса никогда бы не получил возможность для обработки других событий. Пользовательский интерфейс не мог бы ответить на входные данные или обработать сообщения. Он бы никогда не обновил отображение и не ответил бы на нажатие кнопки.
Можно провести поиск простого числа в отдельном потоке, но тогда пришлось бы иметь дело с проблемами синхронизации. С помощью однопотокового подхода можно непосредственно обновить подпись, в которой перечислено наибольшее простое число.
Если разбить задачу вычисления на управляемые фрагменты, можно периодически возвращаться к объекту Dispatcher и событиям обработки. Можно дать приложению WPF возможность обновлять и обрабатывать ввод.
Лучшим способом разбиения времени обработки между вычислением и обработкой события является управление вычислением из объекта Dispatcher. Используя метод InvokeAsync, мы получаем возможность разместить проверки простых чисел в ту же очередь, из которой происходит прорисовка событий пользовательского интерфейса. В приведенном примере запланирована проверка только одного простого числа в каждый момент времени. После завершения проверки простого числа немедленно планируется следующая проверка. Эта проверка выполняется только после обработки ожидающих событий пользовательского интерфейса.
С помощью этого механизма приложение Microsoft Word выполняет проверку орфографии. Проверка орфографии выполняется в фоновом режиме, используя время простоя потока пользовательского интерфейса. Давайте посмотрим на код.
В следующем примере показан код XAML, который создает пользовательский интерфейс.
Внимание
XAML, показанный в этой статье, находится в проекте C#. Visual Basic XAML немного отличается при объявлении резервного класса для XAML.
<Window x:Class="SDKSamples.PrimeNumber"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Prime Numbers" Width="360" Height="100">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
<Button Content="Start"
Click="StartStopButton_Click"
Name="StartStopButton"
Margin="5,0,5,0" Padding="10,0" />
<TextBlock Margin="10,0,0,0">Biggest Prime Found:</TextBlock>
<TextBlock Name="bigPrime" Margin="4,0,0,0">3</TextBlock>
</StackPanel>
</Window>
В следующем примере показан код программной части.
using System;
using System.Windows;
using System.Windows.Threading;
namespace SDKSamples
{
public partial class PrimeNumber : Window
{
// Current number to check
private long _num = 3;
private bool _runCalculation = false;
public PrimeNumber() =>
InitializeComponent();
private void StartStopButton_Click(object sender, RoutedEventArgs e)
{
_runCalculation = !_runCalculation;
if (_runCalculation)
{
StartStopButton.Content = "Stop";
StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}
else
StartStopButton.Content = "Resume";
}
public void CheckNextNumber()
{
// Reset flag.
_isPrime = true;
for (long i = 3; i <= Math.Sqrt(_num); i++)
{
if (_num % i == 0)
{
// Set not a prime flag to true.
_isPrime = false;
break;
}
}
// If a prime number, update the UI text
if (_isPrime)
bigPrime.Text = _num.ToString();
_num += 2;
// Requeue this method on the dispatcher
if (_runCalculation)
StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}
private bool _isPrime = false;
}
}
Imports System.Windows.Threading
Public Class PrimeNumber
' Current number to check
Private _num As Long = 3
Private _runCalculation As Boolean = False
Private Sub StartStopButton_Click(sender As Object, e As RoutedEventArgs)
_runCalculation = Not _runCalculation
If _runCalculation Then
StartStopButton.Content = "Stop"
StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
Else
StartStopButton.Content = "Resume"
End If
End Sub
Public Sub CheckNextNumber()
' Reset flag.
_isPrime = True
For i As Long = 3 To Math.Sqrt(_num)
If (_num Mod i = 0) Then
' Set Not a prime flag to true.
_isPrime = False
Exit For
End If
Next
' If a prime number, update the UI text
If _isPrime Then
bigPrime.Text = _num.ToString()
End If
_num += 2
' Requeue this method on the dispatcher
If (_runCalculation) Then
StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
End If
End Sub
Private _isPrime As Boolean
End Class
Помимо обновления текста в Buttonочереди обработчик StartStopButton_Click
отвечает за планирование первой проверки праймерных чисел, добавив делегат в Dispatcher очередь. Через некоторое время после завершения работы Dispatcher обработчика событий будет выбран делегат для выполнения.
Как упоминалось ранее, InvokeAsync является членом Dispatcher, который размещает делегат для выполнения. В данном сценарии выберем приоритет SystemIdle. Объект Dispatcher будет выполнять данный делегат только при отсутствии важных событий для обработки. Быстродействие пользовательского интерфейса представляет большую важность, чем проверка числа. Также передается новый делегат, представляющий подпрограмму проверки числа.
public void CheckNextNumber()
{
// Reset flag.
_isPrime = true;
for (long i = 3; i <= Math.Sqrt(_num); i++)
{
if (_num % i == 0)
{
// Set not a prime flag to true.
_isPrime = false;
break;
}
}
// If a prime number, update the UI text
if (_isPrime)
bigPrime.Text = _num.ToString();
_num += 2;
// Requeue this method on the dispatcher
if (_runCalculation)
StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}
private bool _isPrime = false;
Public Sub CheckNextNumber()
' Reset flag.
_isPrime = True
For i As Long = 3 To Math.Sqrt(_num)
If (_num Mod i = 0) Then
' Set Not a prime flag to true.
_isPrime = False
Exit For
End If
Next
' If a prime number, update the UI text
If _isPrime Then
bigPrime.Text = _num.ToString()
End If
_num += 2
' Requeue this method on the dispatcher
If (_runCalculation) Then
StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
End If
End Sub
Private _isPrime As Boolean
Этот метод проверяет, является ли следующее нечетное число простым. Если это просто, метод напрямую обновляет bigPrime
TextBlock его обнаружение. Это можно сделать, так как вычисление происходит в том же потоке, который использовался для создания элемента управления. Если использовать отдельный поток для вычислений, пришлось бы применить более сложный механизм синхронизации и выполнять обновления в потоке пользовательского интерфейса. Далее мы продемонстрируем эту ситуацию.
Несколько окон, несколько потоков
Для некоторых приложений WPF требуется несколько окон верхнего уровня. Это идеально приемлемо для одного сочетания thread/Dispatcher для управления несколькими окнами, но иногда несколько потоков делают лучшее задание. Это особенно верно, если есть какой-либо шанс, что одна из окон будет монополизировать поток.
Таким образом работает проводник Windows. Каждое новое окно обозревателя принадлежит исходному процессу, но оно создается под контролем независимого потока. Если обозреватель не отвечает, например при поиске сетевых ресурсов, другие окна обозревателя продолжают реагировать и использовать их.
Мы можем продемонстрировать эту концепцию с помощью следующего примера.
Первые три окна этого образа используют один и тот же идентификатор потока: 1. Два других окна имеют разные идентификаторы потоков: Девять и 4. В правом верхнем углу каждого окна имеется цветной !️ глиф!
В этом примере содержится окно с поворотным ‼️
глифом , кнопкой приостановки и двумя другими кнопками, которые создают новое окно под текущим потоком или в новом потоке. ‼️
Глиф постоянно поворачивается до нажатия кнопки "Пауза", которая приостанавливает поток в течение пяти секунд. В нижней части окна отображается идентификатор потока.
При нажатии кнопки "Пауза" все окна под тем же потоком становятся неответственными. Любое окно под другим потоком продолжает работать нормально.
Следующий пример — XAML в окне:
<Window x:Class="SDKSamples.MultiWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Thread Hosted Window" Width="360" Height="180" SizeToContent="Height" ResizeMode="NoResize" Loaded="Window_Loaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Right" Margin="30,0" Text="‼️" FontSize="50" FontWeight="ExtraBold"
Foreground="Magenta" RenderTransformOrigin="0.5,0.5" Name="RotatedTextBlock">
<TextBlock.RenderTransform>
<RotateTransform Angle="0" />
</TextBlock.RenderTransform>
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="RotatedTextBlock"
Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
<Button Content="Pause" Click="PauseButton_Click" Margin="5,0" Padding="10,0" />
<TextBlock Margin="5,0,0,0" Text="<-- Pause for 5 seconds" />
</StackPanel>
<StackPanel Grid.Row="1" Margin="10">
<Button Content="Create 'Same Thread' Window" Click="SameThreadWindow_Click" />
<Button Content="Create 'New Thread' Window" Click="NewThreadWindow_Click" Margin="0,10,0,0" />
</StackPanel>
<StatusBar Grid.Row="2" VerticalAlignment="Bottom">
<StatusBarItem Content="Thread ID" Name="ThreadStatusItem" />
</StatusBar>
</Grid>
</Window>
В следующем примере показан код программной части.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace SDKSamples
{
public partial class MultiWindow : Window
{
public MultiWindow() =>
InitializeComponent();
private void Window_Loaded(object sender, RoutedEventArgs e) =>
ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}";
private void PauseButton_Click(object sender, RoutedEventArgs e) =>
Task.Delay(TimeSpan.FromSeconds(5)).Wait();
private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
new MultiWindow().Show();
private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
{
Thread newWindowThread = new Thread(ThreadStartingPoint);
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start();
}
private void ThreadStartingPoint()
{
new MultiWindow().Show();
System.Windows.Threading.Dispatcher.Run();
}
}
}
Imports System.Threading
Public Class MultiWindow
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}"
End Sub
Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
Task.Delay(TimeSpan.FromSeconds(5)).Wait()
End Sub
Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
Dim window As New MultiWindow()
window.Show()
End Sub
Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs)
Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint)
newWindowThread.SetApartmentState(ApartmentState.STA)
newWindowThread.IsBackground = True
newWindowThread.Start()
End Sub
Private Sub ThreadStartingPoint()
Dim window As New MultiWindow()
window.Show()
System.Windows.Threading.Dispatcher.Run()
End Sub
End Class
Ниже приведены некоторые сведения, которые необходимо отметить:
Задача Task.Delay(TimeSpan) используется для приостановки текущего потока в течение пяти секунд при нажатии кнопки "Пауза ".
private void PauseButton_Click(object sender, RoutedEventArgs e) => Task.Delay(TimeSpan.FromSeconds(5)).Wait();
Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs) Task.Delay(TimeSpan.FromSeconds(5)).Wait() End Sub
Обработчик
SameThreadWindow_Click
событий неявно отображает новое окно в текущем потоке. ОбработчикNewThreadWindow_Click
событий создает новый поток, который запускает выполнениеThreadStartingPoint
метода, который в свою очередь показывает новое окно, как описано в следующей точке маркера.private void SameThreadWindow_Click(object sender, RoutedEventArgs e) => new MultiWindow().Show(); private void NewThreadWindow_Click(object sender, RoutedEventArgs e) { Thread newWindowThread = new Thread(ThreadStartingPoint); newWindowThread.SetApartmentState(ApartmentState.STA); newWindowThread.IsBackground = true; newWindowThread.Start(); }
Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs) Dim window As New MultiWindow() window.Show() End Sub Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs) Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint) newWindowThread.SetApartmentState(ApartmentState.STA) newWindowThread.IsBackground = True newWindowThread.Start() End Sub
Метод
ThreadStartingPoint
является отправной точкой для нового потока. Новое окно создается под контролем этого потока. WPF автоматически создает новый объект System.Windows.Threading.Dispatcher для управления новым потоком. Все, что нужно сделать для обеспечения функциональности окна — это запустить System.Windows.Threading.Dispatcher.private void ThreadStartingPoint() { new MultiWindow().Show(); System.Windows.Threading.Dispatcher.Run(); }
Private Sub ThreadStartingPoint() Dim window As New MultiWindow() window.Show() System.Windows.Threading.Dispatcher.Run() End Sub
Пример приложения, демонстрирующего основные понятия этого раздела, можно скачать с GitHub для C# или Visual Basic.
Обработка операции блокировки с помощью Task.Run
Обработка блокировки операций в графическом приложении может оказаться трудной задачей. Мы не хотим вызывать методы блокировки от обработчиков событий, так как приложение, как представляется, зависает. Предыдущий пример создал новые окна в собственном потоке, позволяя каждому окну работать независимо друг от друга. Хотя мы можем создать новый поток с System.Windows.Threading.Dispatcherпомощью, после завершения работы сложно синхронизировать новый поток с основным потоком пользовательского интерфейса. Так как новый поток не может напрямую изменить пользовательский интерфейс, необходимо использовать Dispatcher.InvokeAsync( Dispatcher.BeginInvokeили Dispatcher.Invoke) для вставки делегатов в Dispatcher поток пользовательского интерфейса. В конечном итоге эти делегаты выполняются с разрешением на изменение элементов пользовательского интерфейса.
Существует более простой способ запуска кода в новом потоке при синхронизации результатов, асинхронного шаблона на основе задач (TAP). Он основан на Task типах в Task<TResult> System.Threading.Tasks
пространстве имен, которые используются для представления асинхронных операций. TAP использует один метод для представления инициализации и завершения асинхронной операции. Существует несколько преимуществ для этого шаблона:
- Вызывающий
Task
объект может выполнять код асинхронно или синхронно. - Ход выполнения можно сообщить из .
Task
- Вызывающий код может приостановить выполнение и ждать результата операции.
Пример Task.Run
В этом примере мы имитируем вызов удаленной процедуры, который получает прогноз погоды. При нажатии кнопки пользовательский интерфейс обновляется, чтобы указать, что выполняется получение данных, а задача начинается для имитации прогноза погоды. При запуске задачи код обработчика событий кнопки приостанавливается до завершения задачи. После завершения задачи код обработчика событий продолжает выполняться. Код приостановлен и не блокирует остальную часть потока пользовательского интерфейса. Контекст синхронизации WPF обрабатывает приостановку кода, что позволяет WPF продолжать выполняться.
Схема, демонстрирующая рабочий процесс примера приложения. Приложение имеет одну кнопку с текстом "Получение прогноза". Есть стрелка, указывающая на следующий этап приложения после нажатия кнопки, которая представляет собой изображение часов, помещенное в центр приложения, указывающее, что приложение занято получением данных. Через некоторое время приложение возвращает изображение солнца или дождя в зависимости от результата данных.
Пример приложения, демонстрирующего основные понятия этого раздела, можно скачать с GitHub для C# или Visual Basic. Код XAML для этого примера довольно большой и не указан в этой статье. Используйте предыдущие ссылки GitHub для просмотра XAML. XAML использует одну кнопку для получения погоды.
Рассмотрим код для XAML:
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Threading.Tasks;
namespace SDKSamples
{
public partial class Weather : Window
{
public Weather() =>
InitializeComponent();
private async void FetchButton_Click(object sender, RoutedEventArgs e)
{
// Change the status image and start the rotation animation.
fetchButton.IsEnabled = false;
fetchButton.Content = "Contacting Server";
weatherText.Text = "";
((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
// Asynchronously fetch the weather forecast on a different thread and pause this code.
string weather = await Task.Run(FetchWeatherFromServerAsync);
// After async data returns, process it...
// Set the weather image
if (weather == "sunny")
weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
else if (weather == "rainy")
weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
//Stop clock animation
((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
//Update UI text
fetchButton.IsEnabled = true;
fetchButton.Content = "Fetch Forecast";
weatherText.Text = weather;
}
private async Task<string> FetchWeatherFromServerAsync()
{
// Simulate the delay from network access
await Task.Delay(TimeSpan.FromSeconds(4));
// Tried and true method for weather forecasting - random numbers
Random rand = new Random();
if (rand.Next(2) == 0)
return "rainy";
else
return "sunny";
}
private void HideClockFaceStoryboard_Completed(object sender, EventArgs args) =>
((Storyboard)Resources["ShowWeatherImageStoryboard"]).Begin(ClockImage);
private void HideWeatherImageStoryboard_Completed(object sender, EventArgs args) =>
((Storyboard)Resources["ShowClockFaceStoryboard"]).Begin(ClockImage, true);
}
}
Imports System.Windows.Media.Animation
Public Class Weather
Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
' Change the status image and start the rotation animation.
fetchButton.IsEnabled = False
fetchButton.Content = "Contacting Server"
weatherText.Text = ""
DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
' Asynchronously fetch the weather forecast on a different thread and pause this code.
Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
' After async data returns, process it...
' Set the weather image
If weatherType = "sunny" Then
weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
ElseIf weatherType = "rainy" Then
weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
End If
' Stop clock animation
DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
' Update UI text
fetchButton.IsEnabled = True
fetchButton.Content = "Fetch Forecast"
weatherText.Text = weatherType
End Sub
Private Async Function FetchWeatherFromServerAsync() As Task(Of String)
' Simulate the delay from network access
Await Task.Delay(TimeSpan.FromSeconds(4))
' Tried and true method for weather forecasting - random numbers
Dim rand As New Random()
If rand.Next(2) = 0 Then
Return "rainy"
Else
Return "sunny"
End If
End Function
Private Sub HideClockFaceStoryboard_Completed(sender As Object, e As EventArgs)
DirectCast(Resources("ShowWeatherImageStoryboard"), Storyboard).Begin(ClockImage)
End Sub
Private Sub HideWeatherImageStoryboard_Completed(sender As Object, e As EventArgs)
DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Begin(ClockImage, True)
End Sub
End Class
Ниже приведены некоторые подробности, на которые следует обратить внимание.
Обработчик событий кнопки
private async void FetchButton_Click(object sender, RoutedEventArgs e) { // Change the status image and start the rotation animation. fetchButton.IsEnabled = false; fetchButton.Content = "Contacting Server"; weatherText.Text = ""; ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this); // Asynchronously fetch the weather forecast on a different thread and pause this code. string weather = await Task.Run(FetchWeatherFromServerAsync); // After async data returns, process it... // Set the weather image if (weather == "sunny") weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"]; else if (weather == "rainy") weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"]; //Stop clock animation ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage); ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage); //Update UI text fetchButton.IsEnabled = true; fetchButton.Content = "Fetch Forecast"; weatherText.Text = weather; }
Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs) ' Change the status image and start the rotation animation. fetchButton.IsEnabled = False fetchButton.Content = "Contacting Server" weatherText.Text = "" DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me) ' Asynchronously fetch the weather forecast on a different thread and pause this code. Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync) ' After async data returns, process it... ' Set the weather image If weatherType = "sunny" Then weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource) ElseIf weatherType = "rainy" Then weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource) End If ' Stop clock animation DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage) DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage) ' Update UI text fetchButton.IsEnabled = True fetchButton.Content = "Fetch Forecast" weatherText.Text = weatherType End Sub
Обратите внимание, что обработчик событий был объявлен с
async
(илиAsync
с Visual Basic). Метод async позволяет приостановить код при вызове ожидаемого метода, напримерFetchWeatherFromServerAsync
. Это обозначается ключевым словомawait
(илиAwait
с помощью Visual Basic).FetchWeatherFromServerAsync
Пока не завершится, код обработчика кнопки приостановлен и элемент управления возвращается вызывающей объекту. Это аналогично синхронному методу, за исключением того, что синхронный метод ожидает завершения каждой операции в методе, после чего элемент управления возвращается вызывающему объекту.Ожидаемые методы используют контекст потоков текущего метода, который с обработчиком кнопки является потоком пользовательского интерфейса. Это означает, что вызов
await FetchWeatherFromServerAsync();
(илиAwait FetchWeatherFromServerAsync()
visual Basic) приводит к выполнению кода вFetchWeatherFromServerAsync
потоке пользовательского интерфейса, но не выполняется на диспетчере время его выполнения, аналогично тому, как работает однопоточное приложение с длительным примером вычисления . Однако обратите внимание, чтоawait Task.Run
используется. При этом создается новый поток в пуле потоков для указанной задачи вместо текущего потока. ПоэтомуFetchWeatherFromServerAsync
выполняется в собственном потоке.Выборка погоды
private async Task<string> FetchWeatherFromServerAsync() { // Simulate the delay from network access await Task.Delay(TimeSpan.FromSeconds(4)); // Tried and true method for weather forecasting - random numbers Random rand = new Random(); if (rand.Next(2) == 0) return "rainy"; else return "sunny"; }
Private Async Function FetchWeatherFromServerAsync() As Task(Of String) ' Simulate the delay from network access Await Task.Delay(TimeSpan.FromSeconds(4)) ' Tried and true method for weather forecasting - random numbers Dim rand As New Random() If rand.Next(2) = 0 Then Return "rainy" Else Return "sunny" End If End Function
Для упрощения работы в этом примере у нас нет сетевого кода. Вместо этого мы моделируем задержку доступа к сети, задав для нашего нового потока спящий режим в течение четырех секунд. В настоящее время исходный поток пользовательского интерфейса по-прежнему выполняется и отвечает на события пользовательского интерфейса, пока обработчик событий кнопки не будет приостановлен до завершения нового потока. Чтобы продемонстрировать это, мы оставили анимацию запущенной, и вы можете изменить размер окна. Если поток пользовательского интерфейса приостановлен или отложен, анимация не будет отображаться, и вы не могли взаимодействовать с окном.
Task.Delay
По завершении, и мы случайно выбрали наш прогноз погоды, состояние погоды возвращается вызывающей.Обновление пользовательского интерфейса
private async void FetchButton_Click(object sender, RoutedEventArgs e) { // Change the status image and start the rotation animation. fetchButton.IsEnabled = false; fetchButton.Content = "Contacting Server"; weatherText.Text = ""; ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this); // Asynchronously fetch the weather forecast on a different thread and pause this code. string weather = await Task.Run(FetchWeatherFromServerAsync); // After async data returns, process it... // Set the weather image if (weather == "sunny") weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"]; else if (weather == "rainy") weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"]; //Stop clock animation ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage); ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage); //Update UI text fetchButton.IsEnabled = true; fetchButton.Content = "Fetch Forecast"; weatherText.Text = weather; }
Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs) ' Change the status image and start the rotation animation. fetchButton.IsEnabled = False fetchButton.Content = "Contacting Server" weatherText.Text = "" DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me) ' Asynchronously fetch the weather forecast on a different thread and pause this code. Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync) ' After async data returns, process it... ' Set the weather image If weatherType = "sunny" Then weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource) ElseIf weatherType = "rainy" Then weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource) End If ' Stop clock animation DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage) DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage) ' Update UI text fetchButton.IsEnabled = True fetchButton.Content = "Fetch Forecast" weatherText.Text = weatherType End Sub
Когда задача завершится, и поток пользовательского
Task.Run
интерфейса имеет время, вызывающий обработчик событий кнопки, возобновляется. Остальная часть метода останавливает анимацию часов и выбирает изображение для описания погоды. Он отображает это изображение и включает кнопку "Получить прогноз".
Пример приложения, демонстрирующего основные понятия этого раздела, можно скачать с GitHub для C# или Visual Basic.
Технические сведения и точки спотыкания
В следующих разделах описаны некоторые детали и точки спотыкания, которые вы можете столкнуться с многопоточностью.
Вложенная насосная откачка
Иногда невозможно полностью заблокировать поток пользовательского интерфейса. 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
не помечает событие как обработанное после завершения цикла обработки сообщений, событие передается вверх по дереву, даже если оно является очень старым.
Повторное выполнение и блокировка
Механизм блокировки среды CLR не ведет себя точно так же, как можно себе представить; При запросе блокировки поток может полностью прекратить операцию. В действительности поток продолжает получать и обрабатывать сообщения с высоким приоритетом. Это помогает избежать взаимоблокировок и максимально повышает скорость отклика интерфейсов, но может приводить к незначительным ошибкам. Подавляющее большинство времени, когда вы не должны знать ничего об этом, но в редких обстоятельствах (обычно с участием сообщений окна Win32 или компонентов COM STA) это может быть стоит знать.
Большинство интерфейсов построено без учета безопасности потоков, так как разработчик предполагает, что доступ к пользовательскому интерфейсу всегда выполняется не более чем одним потоком. В этом случае предполагается, что неблагоприятные последствия, вносимые одним потоком при изменении среды в непредвиденное время, устраняют механизм взаимного исключения DispatcherObject. Рассмотрим следующий псевдокод:
Большая часть времени, что правильно, но есть времена в WPF, где такая непредвиденная повторное действие действительно может вызвать проблемы. Поэтому в некие ключевые моменты приложение WPF вызывает метод DisableProcessing, который меняет инструкцию блокировки для этого потока, чтобы использовать свободную от повторного входа блокировку вместо обычной блокировки CLR.
Так почему же команда CLR выбрала такое поведение? Это было связано с объектами COM STA и завершением потока. Если объект удаляется сборщиком мусора, его метод Finalize
выполняется не в потоке пользовательского интерфейса, а в выделенном потоке метода завершения. Именно здесь заключена проблема, поскольку объект COM STA, созданный в потоке пользовательского интерфейса, может быть удален только в потоке пользовательского интерфейса. Среда CLR выполняет эквивалент ( BeginInvoke в данном случае используется win32 SendMessage
). Но если поток пользовательского интерфейса занят, поток завершения остановлен, и объект COM STA не может быть удален, что создает серьезную утечку памяти. Поэтому команда CLR создала сложный вызов для формирования такого механизма блокировки.
Задача WPF заключается в том, чтобы избежать неожиданного повторного входа без повторного создания утечки памяти, поэтому мы не блокируем повторное выполнение операций повсюду.
См. также
.NET Desktop feedback