这篇文章由Filip Ekberg为DNC杂志编写。php
自跟随着.NET 4.5 及Visual Studio 2012的C# 5.0起,咱们可以使用涉及到async和await关键字的新的异步模式。有不少不一样观点认为,比起之前咱们看到的,它的可读性和可用性是否更为突出。咱们将经过一个例子来看下它跟如今的怎么不一样。编程
线性代码vs非线性代码
大部分的软件工程师都习惯用一种线性的方式去编程,至少这是他们开始职业生涯时就被这样教导。当一个程序使用线性方式去编写,这意味着它的源代码读起来有的像Figure 1展现的。这就是假设有一个适当的订单系统会帮助咱们从某些地方去取一批订单。c#
即便文章从左或从由开始,人们仍是习惯于从上到下地阅读。若是咱们有某些东西影响到了这个内容的顺序,咱们将会感到困惑同时在这上面比实际须要的事情上花费更多努力。基于事件的程序一般拥有这些非线性的结构。app
基于事件系统的流程是这样的,它在某处发起一个调用同时期待结果经过一个触发的时间传递,Figure 2 展现的很形象的表达了这点。初看这两个序列彷佛不是很大区别,但若是咱们假设GetAllOrders返回空,咱们检索订单列表就没那么直接了当了。异步
不看实际的代码,咱们认为线性方法处理起来更加舒服,同时它更少的有出错的倾向。在这种状况下,错误可能不是实际的运行时错误或者编译错误,可是在使用上的错误;因为缺少明朗。async
基于事件的方法有一个很大的优点;它让咱们使用基于事件的异步模式更为一致。异步编程
在你看到一个方法的时候,你会想去弄明白这方法的目的。这意味着若是你有一个叫ReloadOrdersAndRefreshUI的方法,你想去弄明白这些订单从哪里载入,怎样把它加到UI,当这方法结束的时候会发生什么。在基于事件的方法里,这很难如愿以偿。工具
另外得益于这的是,只要在咱们出发LoadOrdersCompleted事件时,咱们可以在GetAllOrders里写异步代码,返回到调用线程去。this
介绍一个新的模式
让 咱们假设咱们在本身的系统上工做,系统使用上面提到过的OrderHandler以及实际实现是使用一个线性方法。为了模拟一小部分的真是订单系统,OrderHandler和Order以下:spa
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; } }
由于咱们在例子里不使用真是的数据源,咱们须要让它有那么一点更为有趣的。因为这是关于异步编程的,咱们想要在一个异步的方式中请求一些东西。为了模拟这个,咱们简单的加入:
System.Threading.ManualResetEvent(false).WaitOne(2000) in GetAllOrders: public IEnumerable<Order> GetAllOrders() { System.Threading.ManualResetEvent(false).WaitOne(2000); return _orders; }
这里咱们不用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>
在咱们的程序能跑以前,咱们还须要在代码文件里加入一些东西:
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; }
这会带给咱们一个挺好看的应用程序,当咱们在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; })); } }
BackgroundWorker因为基于事件的异步性而被认识,这种模式叫作基于事件异步模式(EAP)。这每每会使代码比之前更乱,同时,因为它使用非线性方式编写,咱们的脑壳要花一段事件才能对它有必定的概念。
但在WinRT中没有BackgroundWorker,因此咱们必须适应新的线性方法,这也是一个好的事情!
咱们对此的解决方法是适应.NET4.5引入的新的模式,async 与 await。当咱们使用async 和 await,就必须同时使用任务并行库(TPL)。原则是每当一个方法须要异步执行,咱们就给它这个标记。这意味着该方法将带着一些咱们等待的东西返回,一个继续点。继续点段所在位置的标记,是由‘awaitable’的标记指明的,此后咱们请求等待任务完成。
基于原始代码,没有BackgroundWorker的话咱们只能对click处理代码作一些小的改变,以便它能应用于异步的方式。首先咱们须要标记该方法为异步的,这简单到只需将关键字加到方法签名:
private async void LoadOrders_Click(object sender, RoutedEventArgs e)
同时使用async和void时须要很当心,标记一个异步的方法返回值为void的惟一缘由,就是由于事件处理代码。当方法不是事件处理者,且返回类型为空时,毫不要标记其为异步的!异步与等待老是同时使用的,若是一个方法标记为异步的但其内部却没有什么可等待的,它将只会以同步方式执行。
所以下一个咱们要作的事情事实上就是保证有一些咱们能等待的事情,在咱们的例子中就是调用GetAllOrders。因为这是最耗费时间的部分,咱们但愿它能够在一个独立的task中执行。咱们只需将这个方法打包于一个期待返回IEnumerable<Order>的task,就像这样:
Task<IEnumerable<Order>>.Factory.StartNew(() => { return orderHandler.GetAllOrders(); });
上面就是咱们要等待的部分,咱们来看看开始咱们有的并对比一下如今咱们有的:
// Before var orders = orderHandler.GetAllOrders(); // After var orders = await Task<IEnumerable<Order>>.Factory.StartNew(() => { return orderHandler.GetAllOrders(); });
当咱们在一个task前增长了等待,订单变量的类型就是task期待返回的类型;在这个例子中是IEnumerable<Order>。这意味着咱们要使这个方法异步,须要惟一作的就是标记它是异步的,而且将对执行时间长的方法的调用封装于一个task以内。
内部发生的事情就是咱们将用一个状态机保存task执行结束的印记。等待代码段的全部代码将被放入一个继续点代码段。若是你对TPL和task的继续点熟悉,这就与之相似,除了咱们到达继续点便回到了调用线程以外!这是一个重要的区别,由于那意味着咱们可使咱们的方法像这样,而不须要任何分派器的调用:
private async void LoadOrders_Click(object sender, RoutedEventArgs e) { OrderLoadingProgress.Visibility = Visibility.Visible; var orderHandler = new OrderHandler(); var orderTask = Task<IEnumerable<Order>>.Factory.StartNew(() => { return orderHandler.GetAllOrders(); }); var orders = await orderTask; Orders.Items.Clear(); foreach (var order in orders) Orders.Items.Add(order); OrderLoadingProgress.Visibility = Visibility.Collapsed; }
正如你看到的,咱们只需在等待代码段以后改变UI上的东西,而不须要使用咱们前面在用EAP或TPL时用到的分派器。如今咱们能够执行这个应用而且装载订单而不锁定UI,而且而后会很漂亮的得到许多订单列表的显示。
新方法带来的好处事显而易见的,它使得代码更线性、更具可读性。 固然,即便是最好的模式,也能写出难看的代码。 异步和待机确实可以使代码更可读、更易于维护。
结论
Async & Await 使得建立一个具备可读性与可维护性的异步解决方案变得很容易。在本文发布前,咱们不得不求助于可能引发困惑的基于事件的方法。因为咱们已处于几乎全部电脑,甚至手机都有至少两个内核的时代,咱们将会看到更多的并行的异步的代码。由于这些使得async & await 很容易,因此在开发阶段引入这个问题已没有必要。咱们能避免因为没有调度程序或调度功能而采用任务或基于事件的异步性所引发的跨线程的问题。随着这个新的模式,咱们能够再也不陷入聚焦于建立可响应可维护的解决方案的思考。
固然,这并不是万能的。总有这个方法也会致使混乱的情形。但只要在适当的地方使用它,将有益于应用的生命周期。