详解C#中的Async和Await用法

 更新时间:2020年6月25日 11:28  点击:1933

 这篇文章由Filip Ekberg为DNC杂志编写。

自跟随着.NET 4.5 及Visual Studio 2012的C# 5.0起,我们能够使用涉及到async和await关键字的新的异步模式。有很多不同观点认为,比起以前我们看到的,它的可读性和可用性是否更为突出。我们将通过一个例子来看下它跟现在的怎么不同。

线性代码vs非线性代码

大部分的软件工程师都习惯用一种线性的方式去编程,至少这是他们开始职业生涯时就被这样教导。当一个程序使用线性方式去编写,这意味着它的源代码读起来有的像Figure 1展示的。这就是假设有一个适当的订单系统会帮助我们从某些地方去取一批订单。

2015713100246385.png (400×432)

 即使文章从左或从由开始,人们还是习惯于从上到下地阅读。如果我们有某些东西影响到了这个内容的顺序,我们将会感到困惑同时在这上面比实际需要的事情上花费更多努力。基于事件的程序通常拥有这些非线性的结构。


基于事件系统的流程是这样的,它在某处发起一个调用同时期待结果通过一个触发的时间传递,Figure 2 展示的很形象的表达了这点。初看这两个序列似乎不是很大区别,但如果我们假设GetAllOrders返回空,我们检索订单列表就没那么直接了当了。

不看实际的代码,我们认为线性方法处理起来更加舒服,同时它更少的有出错的倾向。在这种情况下,错误可能不是实际的运行时错误或者编译错误,但是在使用上的错误;由于缺乏明朗。


基于事件的方法有一个很大的优势;它让我们使用基于事件的异步模式更为一致。

2015713100321237.png (600×332)

 在你看到一个方法的时候,你会想去弄明白这方法的目的。这意味着如果你有一个叫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的模拟器上运行的时候看起来就像这样:

2015713100358213.png (600×371)

看下底部的应用程序工具栏, 通过按这个在右手边的菜单的图标进入基本的触摸模式,然后从下往上刷。
 现在当你按下加载订单按钮的时候,你会注意到你看不到进度条同时按钮保持在被按下状态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,并且然后会很漂亮的获得许多订单列表的显示。

2015713100421536.png (600×371)

 新方法带来的好处事显而易见的,它使得代码更线性、更具可读性。 当然,即使是最好的模式,也能写出难看的代码。 异步和待机确实能够使代码更可读、更易于维护。

结论

Async & Await 使得创建一个具有可读性与可维护性的异步解决方案变得很容易。在本文发布前,我们不得不求助于可能引起困惑的基于事件的方法。由于我们已处于几乎所有电脑,甚至手机都有至少两个内核的时代,我们将会看到更多的并行的异步的代码。因为这些使得async & await 很容易,所以在开发阶段引入这个问题已没有必要。我们能避免由于没有调度程序或调度功能而采用任务或基于事件的异步性所引起的跨线程的问题。随着这个新的模式,我们可以不再陷入聚焦于创建可响应可维护的解决方案的思考。


当然,这并非万能的。总有这个方法也会导致混乱的情形。但只要在适当的地方使用它,将有益于应用的生命周期。

[!--infotagslink--]

相关文章

  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
  • c#自带缓存使用方法 c#移除清理缓存

    这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
  • C#学习笔记- 随机函数Random()的用法详解

    下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#中list用法实例

    这篇文章主要介绍了C#中list用法,结合实例形式分析了C#中list排序、运算、转换等常见操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25