GUI程序中的异步操作
尽管本章目前的所有代码均针对控制台应用程序,但实际上异步方法在GUI程序中尤为
有用。
原因是GUI程序在设计上就要求所有的显示变化都必须在主GUI线程中完成,如点击按钮、
展示标签、移动窗体等。Windows程序是通过消息来实现这一点的,消息被放入由消息泵管理的
消息队列中。
消息泵从队列中取出一条消息,并调用它的处理程序(handler)代码。当处理程序代码完成
时,消息泵获取下一条消息并循环这个过程。
由于这种架构,处理程序代码就必须短小精悍六这样才不至于挂起并阻碍其他GUI行为的
处理。如果某个消息的处理程序代码耗时过长,消息队列中的消息会产生积压,程序将失去响应,
因为在那个长时间运行的处理程序完成之前,无法处理任何消息。
图21-11展示了一个WPF程序中两个版本的窗体。窗体由状态标签及其下方的按钮组成。
开发者的目的是,程序用户会点击按钮,而按钮的处理程序代码会执行以下操作:
- 禁用按钮,这样在处理程序执行期间用户就不能再次点击了;
- 将标签文本改为Doings懨这样用户就会知道程序正在工作;
- 让程序休眠4秒钟一一一模拟某个工作;
- 将标签文本改为原始文本,并启用按钮。
图2-11中右侧的截屏展示了开发者希望在按钮按下的4秒之内窗体的样子。然而事实并非
如此。当开发者点击按钮后,什么都没有发生。而且如果在点击按钮后移动窗体,会发现它已经
冻结,不会移动一一一直到4秒之后,窗体才突然出现在新位置。

要使用Visual Studio重新创建这个名为MessagePump的WPF程序,步骤如下。
(1)选择File---New---Project菜单项,弹出NewProject窗口。
(2)在窗口左侧的面板内,展开lnstalled Templates(如果没有展开的话)。
(3)在C#类别中点击Windows条目,将在中间面板中弹出已安装的Windows Classic Desktop
程序模板。
(4)点击WPFApp(.NET框架),在窗口下方的Name文本框中输人MessagePumpo在其下方
选择一个位置,并点击OK按钮。
(5)将中的标记修改为下面的代码,在窗体中创建状态标签和按钮。
xml
<Window x:Class="MessagePump.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Pump" Height="120" Width="200" >
<StackPanel>
<Label Name="lblStatus" Margin="10,5,10,0">Not Doing Anything</Label>
<Button Name="btnDoStuff" Content="Do Stuff" HorizontalAlignment="Left"
Margin="10,5" Padding="5,2" Click="btnDoStuff_Click"/>
</StackPanel>
</Window>
(6)将代码隐藏文件MainWindow.xaml.cs修改为如下所示的c#代码。
csharp
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace MessagePump
{
public partical class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void btnDoStuff_Click(object sender, RotedEventArgs e)
{
btnDoStuff.IsEnabled = false;
lblStatus.Content = "Doing Stuff";
Thread.Sleep(4000);
lblStatus.Content = "Not Doing Anything";
btnDoStuff_Click.IsEnabled = true;
}
}
}
运行程序,你会发现其行为与之前的描述完全一致,即按钮没有禁用,状态标签也没有改变,
如果你移动窗体,4秒后它才会移动。
这个奇怪行为的原因其实非简单。图21-12展示了这种情形。点击按钮时,按钮的Click
消息放入消息队列。消息泵从队列中移除该消息并开始处理点击按钮的处理程序代码,即
btnDostuff_Click方法。btnDoStuff_Click处理程序将我们希望触发的行为的消息放人队列,如
右边的图所示。但在处理程序本身退出(即休眠4秒并退出)之前,这些消息都无法执行。然后
所有的行为都发生了,但速度太快,肉眼根本看不见。

消息泵分发消息队列中的消息。在按钮消息处理程序执行的时候,其他行
为的消息压入队列,但在其完成之前都无法执行
。
csharp
private async void btnDoStuff_Click(object sender,RoutedEventArgs e)
{
btnDoStuff_Click.IsEnabled = false;
lblStatus.Content = "Doint Stuff";
await Task.Delay(4000);
lblStatus.Content = "Not Doing Anything";
btnDoStuff_Click.IsEnabled = true;
}
Task.Yield
Task.Yield方法创建一个立即返回的awaitable。等待一个Yield可以让异步方法在执行后
续部分的同时返回到调用方法。可以将其理解成离开当前的消息队列,回到队列末尾,让处理器
有时间处理其他任务。
下面的示例代码展示了一个异步方法,程序每执行某个循环1000次就移交一次控制权。每
次执行方法,线程中的其他任务就得以执行。
csharp
static class DoStuff
{
public static async Task<int> FindSeriersSum(int i1)
{
int sum = 0;
for (int i = 0; i < i1; i++)
{
sum += i;
if (i % 1000 == 0)
await Task.Yield();
}
return sum;
}
}
class Program
{
static void Main()
{
Task<int> value = DoStuff.FindSeriersSum(1_000_000);
CountBig(100_000); CountBig(100_000);
CountBig(100_000); CountBig(100_000);
Console.WriteLine($"Sum:{value.Result}");
}
private static void CountBig(int p)
{
for (int i = 0; i < p; i++)
;
}
}
Yield方法在GUI程序中非常有用,可以中断大量工作,让其他任务使用处理器。