C# 中的 Async 和 Await
这篇文章由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;
}
}
因为我们在例子里不使用真是的数据源,我们需要让它有那么一点更为有趣的。由于这是关于异步编程的,我们想要在一个异步的方式中请求一些东西。为了模拟这个,我们简单的加入:
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 很容易,所以在开发阶段引入这个问题已没有必要。我们能避免由于没有调度程序或调度功能而采用任务或基于事件的异步性所引起的跨线程的问题。随着这个新的模式,我们可以不再陷入聚焦于创建可响应可维护的解决方案的思考。
当然,这并非万能的。总有这个方法也会导致混乱的情形。但只要在适当的地方使用它,将有益于应用的生命周期。
本文的完整代码下载: http://bit.ly/dncmag-asaw (Github)