这篇文章由Filip Ekberg为DNC杂志编写。
自跟随着.NET 4.5 及Visual Studio 2012的C# 5.0起,我们能够使用涉及到async和await关键字的新的异步模式。有很多不同观点认为,比起以前我们看到的,它的可读性和可用性是否更为突出。我们将通过一个例子来看下它跟现在的怎么不同。
线性代码vs非线性代码
大部分的软件工程师都习惯用一种线性的方式去编程,至少这是他们开始职业生涯时就被这样教导。当一个程序使用线性方式去编写,这意味着它的源代码读起来有的像Figure 1展示的。这就是假设有一个适当的订单系统会帮助我们从某些地方去取一批订单。
即使文章从左或从由开始,人们还是习惯于从上到下地阅读。如果我们有某些东西影响到了这个内容的顺序,我们将会感到困惑同时在这上面比实际需要的事情上花费更多努力。基于事件的程序通常拥有这些非线性的结构。
基于事件系统的流程是这样的,它在某处发起一个调用同时期待结果通过一个触发的时间传递,Figure 2 展示的很形象的表达了这点。初看这两个序列似乎不是很大区别,但如果我们假设GetAllOrders返回空,我们检索订单列表就没那么直接了当了。
不看实际的代码,我们认为线性方法处理起来更加舒服,同时它更少的有出错的倾向。在这种情况下,错误可能不是实际的运行时错误或者编译错误,但是在使用上的错误;由于缺乏明朗。
基于事件的方法有一个很大的优势;它让我们使用基于事件的异步模式更为一致。
在你看到一个方法的时候,你会想去弄明白这方法的目的。这意味着如果你有一个叫ReloadOrdersAndRefreshUI的方法,你想去弄明白这些订单从哪里载入,怎样把它加到UI,当这方法结束的时候会发生什么。在基于事件的方法里,这很难如愿以偿。
另外得益于这的是,只要在我们出发LoadOrdersCompleted事件时,我们能够在GetAllOrders里写异步代码,返回到调用线程去。
介绍一个新的模式
让 我们假设我们在自己的系统上工作,系统使用上面提到过的OrderHandler以及实际实现是使用一个线性方法。为了模拟一小部分的真是订单系统,OrderHandler和Order如下:
class Order { public string OrderNumber { get; set; } public decimal OrderTotal { get; set; } public string Reference { get; set; } } class OrderHandler { private readonly IEnumerable<Order> _orders; public OrderHandler() { _orders = new[] { new Order {OrderNumber = "F1", OrderTotal = 100, Reference = "Filip"}, new Order {OrderNumber = "F1", OrderTotal = 100, Reference = "Filip"} }; } public IEnumerable<Order> GetAllOrders() { return _orders; } }</div>
因为我们在例子里不使用真是的数据源,我们需要让它有那么一点更为有趣的。由于这是关于异步编程的,我们想要在一个异步的方式中请求一些东西。为了模拟这个,我们简单的加入:
System.Threading.ManualResetEvent(false).WaitOne(2000) in GetAllOrders: public IEnumerable<Order> GetAllOrders() { System.Threading.ManualResetEvent(false).WaitOne(2000); return _orders; }</div>
这里我们不用Thread.Sleep的原因是这段代码将会加入到Windows8商店应用程序。这里的目的是在这里我们将会为我们的加载订单列表的Windows8商店应用程序放置一个可以按的按钮。然后,我们可以比较下用户体验和在之前加入的异步代码。
如果你已经创建了一个空的Windows商店应用程序项目,你可以加入如下的XAML到你的MainPage.xml:
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock x:Name="pageTitle" Margin="120,0,0,0" Text="Order System" Style="{StaticResource PageHeaderTextStyle}" Grid.Column="1" IsHitTestVisible="false"/> <StackPanel Grid.Row="1" Margin="120,50,0,0"> <TextBlock x:Name="Information" /> <ProgressBar x:Name="OrderLoadingProgress" HorizontalAlignment="Left" Foreground="White" Visibility="Collapsed" IsIndeterminate="True" Width="100"> <ProgressBar.RenderTransform> <CompositeTransform ScaleX="5" ScaleY="5" /> </ProgressBar.RenderTransform> </ProgressBar> <ListView x:Name="Orders" DisplayMemberPath="OrderNumber" /> </StackPanel> <AppBar VerticalAlignment="Bottom" Grid.Row="1"> <Button Content="Load orders" x:Name="LoadOrders" Click="LoadOrders_Click" /> </AppBar> </Grid></div>
在我们的程序能跑之前,我们还需要在代码文件里加入一些东西:
public MainPage() { this.InitializeComponent(); Information.Text = "No orders have been loaded yet."; } private void LoadOrders_Click(object sender, RoutedEventArgs e) { OrderLoadingProgress.Visibility = Visibility.Visible; var orderHandler = new OrderHandler(); var orders = orderHandler.GetAllOrders(); OrderLoadingProgress.Visibility = Visibility.Collapsed; }</div>
这会带给我们一个挺好看的应用程序,当我们在Visual Studio 2012的模拟器上运行的时候看起来就像这样:
看下底部的应用程序工具栏, 通过按这个在右手边的菜单的图标进入基本的触摸模式,然后从下往上刷。
现在当你按下加载订单按钮的时候,你会注意到你看不到进度条同时按钮保持在被按下状态2秒。这是由于我们把应用程序锁定了。
以前我们可以通过在一个BackgroundWorker里封装代码来解决问题。当完成的时候,它会在我们为改变UI而已调用的委托中出发一个事件。这是一种非线性的方法,但往往会把代码的可读性搞得糟糕。在一个非WinRT的订单应用程序,使用BackgroundWorker应该看起来像这样:
public sealed partial class MainPage : Page { private BackgroundWorker _worker = new BackgroundWorker(); public MainPage() { InitializeComponent(); _worker.RunWorkerCompleted += WorkerRunWorkerCompleted; _worker.DoWork += WorkerDoWork; } void WorkerDoWork(object sender, DoWorkEventArgs e) { var orderHandler = new OrderHandler(); var orders = orderHandler.GetAllOrders(); } private void LoadOrders_Click(object sender, RoutedEventArgs e) { OrderLoadingProgress.Visibility = Visibility.Visible; _worker.RunWorkerAsync(); } void WorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { Dispatcher.BeginInvoke(new Action(() => { // Update the UI OrderLoadingProgress.Visibility = Visibility.Collapsed; })); } }</div>
BackgroundWorker由于基于事件的异步性而被认识,这种模式叫做基于事件异步模式(EAP)。这往往会使代码比以前更乱,同时,由于它使用非线性方式编写,我们的脑袋要花一段事件才能对它有一定的概念。
但在WinRT中没有BackgroundWorker,所以我们必须适应新的线性方法,这也是一个好的事情!
我们对此的解决方法是适应.NET4.5引入的新的模式,async 与 await。当我们使用async 和 await,就必须同时使用任务并行库(TPL)。原则是每当一个方法需要异步执行,我们就给它这个标记。这意味着该方法将带着一些我们等待的东西返回,一个继续点。继续点段所在位置的标记,是由‘awaitable'的标记指明的,此后我们请求等待任务完成。
基于原始代码,没有BackgroundWorker的话我们只能对click处理代码做一些小的改变,以便它能应用于异步的方式。首先我们需要标记该方法为异步的,这简单到只需将关键字加到方法签名:
private async void LoadOrders_Click(object sender, RoutedEventArgs e)</div>
同时使用async和void时需要很小