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 ...
随机推荐
- Python学习 :集合
集合 Set 集合的创建 集合的创建只有一种方式 集合中的元素必须是不可变的数据类型 集合是无序的,可以通过 for 循环来遍历或者迭代器进行筛选 s=set('xiaoming') s1=['ale ...
- java 异常与捕获
几乎所有的代码里面都会出现异常,为了保证程序在出现异常之后可以正常执行完毕,就需要进行异常处理. 先来看一下异常的继承类结构: 所有的异常都是由Throwable继承而来,我们来看他下面的两个子类Er ...
- 树莓派编译程序时报错:virtual memory exhausted: Cannot allocate memory
一.原因分析: 树莓派内存太小,编译程序会出现virtual memory exhausted: Cannot allocate memory的问题,可以用swap扩展内存的方法. 二.解决方法: 安 ...
- 北京Uber优步司机奖励政策(12月25日)
滴快车单单2.5倍,注册地址:http://www.udache.com/ 如何注册Uber司机(全国版最新最详细注册流程)/月入2万/不用抢单:http://www.cnblogs.com/mfry ...
- 使用navicat连接Mysql8.0出现2059错误
一. 进入MySQL,打开要用navicat连接的数据库 二.打开运行以下代码: ALTER USER 'root'@'localhost' IDENTIFIED BY '你的mysql密码' PAS ...
- selenium元素定位(三)
使用selenium就不可避免的要提到界面元素定位,通过元素定位来实现一系列的逻辑操作. selenium提供了8中元素定位的方式: id.name.class name.tag name.link ...
- TW实习日记:第19天
今天一早上改完信息门户的代码之后,发现接口又出了问题,查了半天都不知道,原来又是网端的问题...真是心累啊,调整了一些细节样式,以及终于把企业微信的消息推送功能做完了.关键就在于有个表存放微信id的字 ...
- lintcode12 带最小值操作的栈
实现一个带有取最小值min方法的栈,min方法将返回当前栈中的最小值. 你实现的栈将支持push,pop 和 min 操作,所有操作要求都在O(1)时间内完成. 建一个栈helpStack,用来存放从 ...
- python程序设计——函数设计与调用
一.函数定义与调用 def 函数名([参数列表]): '''注释''' 函数体 # 输出小于n的斐波那契数 >>def fib(n): a,b=1,1 while a < n: pr ...
- matconv-GPU 编译问题
如出现以下错误: 1 error detected in the compilation of "C:/Users/Justin/AppData/Local/Temp/tmpxft_0000 ...