Windows Presentation Foundation (WPF)旨在使开发人员免受线程处理的困难。 因此,大多数 WPF 开发人员不会编写使用多个线程的接口。 由于多线程程序复杂且难以调试,因此在存在单线程解决方案时应避免它们。
但是,无论架构有多好,任何 UI 框架都无法为各种问题提供单线程解决方案。 WPF 已经很接近了,但在某些情况下,多个线程仍然可以提高用户界面(UI)的响应能力或应用程序性能。 在讨论一些背景材料后,本文将探讨其中一些情况,然后最后讨论一些较低级别的细节。
注释
本主题讨论使用 InvokeAsync 异步调用的方法进行线程处理。 该InvokeAsync
方法采用Action或Func<TResult>作为参数,并返回具有Task属性的DispatcherOperation或DispatcherOperation<TResult>。 您可以将await
关键字与DispatcherOperation或关联的Task一起使用。 如果需要同步等待DispatcherOperation或DispatcherOperation<TResult>返回的Task,请调用DispatcherOperationWait扩展方法。 调用 Task.Wait 将导致 死锁。 有关使用 Task 执行异步操作的详细信息,请参阅 基于任务的异步编程。
若要进行同步调用,请使用 Invoke 该方法,该方法还具有采用委托 Action或 Func<TResult> 参数的重载。
概述和调度程序
通常,WPF 应用程序从两个线程开始:一个用于处理呈现,另一个用于管理 UI。 当 UI 线程接收输入、处理事件、绘制屏幕和运行应用程序代码时,呈现线程实际上在后台隐藏。 大多数应用程序使用单个 UI 线程,尽管在某些情况下最好使用多个。 稍后我们将用一个示例来讨论这一点。
UI 线程将工作项排入名为 Dispatcher 的对象内。 该 Dispatcher 选项按优先级选择工作项,并运行每个项以完成。 每个 UI 线程必须至少有一个 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 线程为其执行某项操作。 它通过向 UI 线程注册工作项Dispatcher来实现这一功能。 该 Dispatcher 类提供用于注册工作项的方法: Dispatcher.InvokeAsync、 Dispatcher.BeginInvoke和 Dispatcher.Invoke。 这些方法安排委托对象的执行。
Invoke
是同步调用 - 也就是说,在 UI 线程实际完成执行委托之前,它不会返回。
InvokeAsync
并且 BeginInvoke
是异步的,并立即返回。
按 Dispatcher 优先级对队列中的元素进行排序。 向队列添加元素 Dispatcher 时,可以指定十个级别。 这些优先级在DispatcherPriority 枚举中得到维护。
具有长时间运行的计算的单线程应用
大多数图形用户界面(GUI)在等待生成事件以响应用户交互时,花费大部分时间处于空闲状态。 通过仔细编程,这种空闲时间可以建设性地使用,而不会影响 UI 的响应能力。 WPF 线程模型不允许输入中断在 UI 线程中进行的操作。 这意味着,必须确保定期返回 Dispatcher 以处理挂起的输入事件,以免在它们失效之前过时。
演示本部分概念的示例应用可从适用于 C# 或 Visual Basic 的 GitHub 下载。
请看下面的示例:
这个简单的应用程序从 3 开始向上计数,搜索质数。 当用户单击“ 开始 ”按钮时,搜索将开始。 当程序找到质数时,它会将其发现更新到用户界面。 在任何时候,用户都可以停止搜索。
虽然质数的搜索过程简单,但是它可能无限进行,这带来了一些困难。 如果我们在按钮的单击事件处理程序内处理了整个搜索,我们绝不会让 UI 线程处理其他事件。 UI 无法响应输入或处理消息。 它永远不会重绘,也永远不会响应按钮单击。
我们可以在单独的线程中执行质数搜索,但随后需要处理同步问题。 通过单线程方法,我们可以直接更新列出最大质数的标签。
如果将计算任务分解为可管理的区块,可以定期返回到 Dispatcher 和处理事件。 我们可以为 WPF 提供重绘和处理输入的机会。
在计算和事件处理之间拆分处理时间的最佳方法是从Dispatcher管理计算。 通过使用 InvokeAsync 方法,我们可以在绘制 UI 事件的同一队列中安排质数检查。 在我们的示例中,我们一次只计划一个质数检查。 质数检查完成后,我们将立即安排下一次检查。 只有在处理挂起的 UI 事件后,此检查才会继续。
Microsoft Word 使用此机制完成拼写检查。 拼写检查在后台使用 UI 线程的空闲时间完成。 让我们看看代码。
以下示例演示创建用户界面的 XAML。
重要
本文中显示的 XAML 来自 C# 项目。 为 XAML 声明后盾类时,Visual Basic 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 执行此委托。 UI 响应能力比数字检查更重要。 我们还传递一个新的委托,用于表示数字检查例程。
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 以反映其发现。 我们可以执行此作,因为计算发生在用于创建控件的同一线程中。 如果选择使用单独的线程进行计算,则必须使用更复杂的同步机制并在 UI 线程中执行更新。 接下来我们将演示这种情况。
多个窗口,多个线程
某些 WPF 应用程序需要多个顶级窗口。 一个线程/调度程序组合管理多个窗口是完全可以接受的,但有时多个线程会做得更好。 在某个窗口有可能垄断线程的情况下,这一点尤其适用。
Windows 资源管理器以这种方式工作。 每个新的资源管理器窗口都属于原始进程,但它是在独立线程的控制下创建的。 当资源管理器变得非响应时(例如在查找网络资源时),其他资源管理器窗口将继续响应且可用。
可以使用以下示例演示此概念。
此映像的前三个窗口共享相同的线程标识符:1。 另外两个窗口具有不同的线程标识符:9 和 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) 用于在按下 暂停 按钮时导致当前线程暂停 5 秒。
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
演示本部分概念的示例应用可从适用于 C# 或 Visual Basic 的 GitHub 下载。
使用 Task.Run 处理阻塞操作
在图形应用程序中处理阻塞操作可能会很困难。 我们不想从事件处理程序调用阻止方法,因为应用程序似乎冻结了。 上一个示例在其自己的线程中创建了新窗口,让每个窗口彼此独立运行。 虽然我们可以创建一个新线程 System.Windows.Threading.Dispatcher,但完成工作后,很难将新线程与主 UI 线程同步。 由于新线程无法直接修改 UI,因此我们必须使用 Dispatcher.InvokeAsync, Dispatcher.BeginInvoke或者 Dispatcher.Invoke,将委托 Dispatcher 插入到 UI 线程中。 最终,这些委托在获得修改 UI 元素权限后被执行。
有一种更简单的方法,可以在新线程上运行代码并同步结果,即基于任务的异步模式(TAP)。 它基于Task命名空间中的System.Threading.Tasks
类型和Task<TResult>类型,用于表示异步作。 TAP 使用单个方法来表示异步作的启动和完成。 此模式有一些好处:
- 调用方
Task
可以选择异步或同步运行代码。 - 进度可以从
Task
报告。 - 调用代码可以暂停执行并等待作的结果。
Task.Run 示例
在此示例中,我们模拟用于检索天气预报的远程过程调用。 单击该按钮时,UI 将更新为指示数据提取正在进行中,同时开始模拟提取天气预报的任务。 启动任务后,按钮事件处理程序代码将挂起,直到任务完成。 任务完成后,事件处理程序代码将继续运行。 代码执行已暂停,不会阻塞 UI 线程的其他部分。 WPF 的同步上下文负责管理代码的挂起,以便允许 WPF 继续运行。
示例应用的工作流演示图。 该应用有一个按钮,按钮上写着“获取天气预报”。 按下按钮后,有一个箭头指向应用的下一阶段,这是放置在应用中心处的时钟图像,指示应用正在忙于提取数据。 一段时间后,应用会返回太阳或雨云的图像,具体取决于数据的结果。
演示本部分概念的示例应用可从适用于 C# 或 Visual Basic 的 GitHub 下载。 此示例的 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) 声明的。 在调用诸如FetchWeatherFromServerAsync
之类的等待方法时,“async”方法允许代码暂时挂起。 这由await
(或Await
Visual Basic) 关键字指定。 在FetchWeatherFromServerAsync
完成之前,按钮的处理程序代码将暂停,控件将返回给调用方。 这类似于同步方法,不同之处在于,同步方法等待方法中的每个作完成,之后控件将返回到调用方。等待的方法利用当前方法的线程上下文,而在按钮处理程序的情况下,这个上下文是 UI 线程。 这意味着调用
await FetchWeatherFromServerAsync();
(或者在 Visual Basic 中使用Await FetchWeatherFromServerAsync()
)会导致代码在FetchWeatherFromServerAsync
UI 线程上运行,但它并不是在调度程序有时间运行时才执行,类似于 单线程应用进行长时间计算 示例的操作。 但是,请注意,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
为了简单起见,本示例中实际上没有任何网络代码。 相反,我们通过将新线程置于睡眠状态四秒来模拟网络访问延迟。 在此期间,原始 UI 线程仍在运行并响应 UI 事件,同时按钮的事件处理程序暂停,直到新线程完成。 为了演示这一点,我们留下了一个正在运行的动画,你可以调整窗口的大小。 如果 UI 线程已暂停或延迟,则不会显示动画,并且无法与窗互。
当
Task.Delay
完成时,我们随机选择了天气预报,并将天气状态反馈给呼叫方。更新 UI
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
当任务完成并且 UI 线程有时间时,
Task.Run
的调用方,即按钮的事件处理程序,将会恢复。 该方法的其余部分停止时钟动画,并选择一个图像来描述天气。 它显示此图像并启用“获取预测”按钮。
演示本部分概念的示例应用可从适用于 C# 或 Visual Basic 的 GitHub 下载。
技术细节和绊脚石点
以下部分介绍了多线程处理时可能会遇到的一些详细信息和绊脚点。
嵌套泵系统
有时完全锁定 UI 线程是不可行的。 让我们考虑 MessageBox 类的 Show 方法。 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)的锁定机制并不像人们预期的那样运行;在请求锁时,一个线程可能会完全停止运行。 实际上,线程继续接收和处理高优先级消息。 这有助于防止死锁并使接口保持最低限度的响应能力,但它引入了产生一些细微 bug 的可能性。 绝大多数情况下,你不需要知道有关此事的任何内容,但在极少数情况下(通常涉及 Win32 窗口消息或 COM STA 组件),这值得了解。
大多数接口不是使用线程安全性构建的,因为开发人员在假设 UI 永远不会被多个线程访问的情况下工作。 在这种情况下,单线程可能会在意想不到的时间改变环境,从而导致相互排斥机制应该解决的那些问题产生不良影响 DispatcherObject 。 请考虑以下伪代码:
大多数时候,这样做是对的,但在 WPF 中,有时这种不可预见的重复调用确实会引发问题。 因此,在某些关键时间,WPF 调用 DisableProcessing 会更改该线程的锁定指令,使其使用 WPF 的无重入锁,而不是通常的 CLR 锁。
那么,为什么 CLR 团队选择了此行为? 它与 COM STA 对象和最终化线程有关。 对象被垃圾回收时,其 Finalize
方法会在专用终结器线程上运行,而不是在 UI 线程上。 问题在于,因为在 UI 线程上创建的 COM STA 对象只能在 UI 线程上释放。 CLR 执行相当于BeginInvoke(在本例中使用 Win32 的SendMessage
)。 但是,如果 UI 线程繁忙,终结器线程将停止,COM STA 对象无法释放,这会产生严重的内存泄漏。 因此,CLR 团队做出了艰难的决定,使锁按照当前的方式运作。
WPF 的任务是避免意外的重新进入而不重新引入内存泄漏,这就是为什么我们没有在所有地方阻止重新进入。