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)
C# 中的 Async 和 Await的更多相关文章
- [译] C# 5.0 中的 Async 和 Await (整理中...)
C# 5.0 中的 Async 和 Await [博主]反骨仔 [本文]http://www.cnblogs.com/liqingwen/p/6069062.html 伴随着 .NET 4.5 和 V ...
- 在MVC中使用async和await的说明
首先,在mvc中如果要用纯异步请不要使用async和await,可以直接使用Task.Run. 其次,在mvc中使用async和await可以让系统开新线程处理Task的代码,同时不必等Task执行结 ...
- C# 中的Async 和 Await 的用法详解
众所周知C#提供Async和Await关键字来实现异步编程.在本文中,我们将共同探讨并介绍什么是Async 和 Await,以及如何在C#中使用Async 和 Await. 同样本文的内容也大多是翻译 ...
- ES7中的async和await
ES7中的async和await 在上一章中,使用Promise将原本的回调方式转换为链式操作,这就将一个个异步执行的操作串在一条同步线上了.下一次的操作必须等待当前操作的结束. 使用Promise的 ...
- async和await的使用总结 ~ 竟然一直用错了c#中的async和await的使用。。
对于c#中的async和await的使用,没想到我一直竟然都有一个错误.. ..还是总结太少,这里记录下. 这里以做早餐为例 流程如下: 倒一杯咖啡. 加热平底锅,然后煎两个鸡蛋. 煎三片培根. 烤两 ...
- ES2017 中的 Async 和 Await
ES2017 在 6 月最终敲定了,随之而来的是广泛的支持了我最喜欢的最喜欢的JavaScript功能: async(异步) 函数.如果你也曾为异步 Javascript 而头疼,那么这个就是为你设计 ...
- .NET中的async和await关键字使用及Task异步调用实例
其实早在.NET 4.5的时候M$就在.NET中引入了async和await关键字(VB为Async和Await)来简化异步调用的编程模式.我也早就体验过了,现在写一篇日志来记录一下顺便凑日志数量(以 ...
- es2017中的async和await要点
1. async和await最关键的用途是以同步的写法实现了异步调用,是对Generator异步方法的简化和改进.使用Generator实现异步的缺点如下: 得有一个任务执行器来自动调用next() ...
- ES7中的async 和 await
async 和 await 一个函数如果加上 async ,那么该函数就会返回一个 Promise async function test() { return "1" } con ...
随机推荐
- java 用接口计算圆柱的体积和面积并添加颜色
import java.util.Scanner; class Test { public static void main(String[] args) { fangfa i = new fangf ...
- 在CentOS7中搭建Zookeeper集群
前几天装了CentOS7.并安装了一些基本的工具,现在我手上有三台机器:分别是master,slave1,slave2. 今天我将搭建zookeeper,使用的版本是zookeeper-3.4.11. ...
- java生成PDF,并下载到本地
1.首先要写一个PDF工具类,以及相关工具 2.PDF所需jar包 iText是一种生成PDF报表的Java组件 freemarker是基于模板来生成文本输出 <dependency> & ...
- 设置cell高度的两种方法(label高度的可变引起cell高度可变的情况)
第一种:(iOS8以后可用) 在Xib或stroyboard中(代码也可以) 利用AutoLayout设置好label的约束(比如可以设置四个边都距离屏幕50等方式,必须四个边都要固定好). 在代码部 ...
- 天嵌IMX6开发板测试-第一篇
1.看下开发板介绍 品牌: 天嵌 CPU型号: NXP i.MX6Q 架构: Cortex_A9 主频: *1GHz 内存: 2GB DDR3 存储: 8GB eMMC FLA(64GB可扩) 2. ...
- PS 抠图和添加背景图
1.打开需要抠的图--然后使用套索类工具,魔棒类工具,钢笔类工具均可选择需要扣的图片范围任何在Delete(如果抠反了可以进行反选Ctrl +shift+I) 2.然后把任一一张背景图直接拖到PS里面 ...
- 第3章 TCP协议详解
第3章 TCP协议详解 3.1 TCP服务的特点 传输协议主要有两个:TCP协议和UDP协议,TCP协议相对于UDP协议的特点是 面向连接使用TCP协议通信的双方必须先建立连接,完成数据交换后,通信双 ...
- 【radio-group、radio】 单选项组件说明
radio-group组件是包裹radio组件的容器 原型: <radio-group bindchange="[EventHandle]"> <radio .. ...
- yii的学习笔记 基本结构 自用
Yii 学习笔记 W:YII是什么? Q:Yii 是一个基于组件的高性能 PHP 框架,用于快速开发大型 Web 应用.它使Web开发中的 可复用度最大化,可以显著提高你的Web应用开发速度.Yii ...
- 一种跨平台的C++遍历目录的方法
参考了网络上各路大神的实现方法.主要使用了io.h库 #include <iostream> #include <cstring> #include <io.h> ...