异步编程系列第04章 编写Async方法

Posted 蜗牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了异步编程系列第04章 编写Async方法相关的知识,希望对你有一定的参考价值。

写在前面

  在学异步,有位园友推荐了《async in C#5.0》,没找到中文版,恰巧也想提高下英文,用我拙劣的英文翻译一些重要的部分,纯属娱乐,简单分享,保持学习,谨记谦虚。

  如果你觉得这件事儿没意义翻译的又差,尽情的踩吧。如果你觉得值得鼓励,感谢留下你的赞,愿爱技术的园友们在今后每一次应该猛烈突破的时候,不选择知难而退。在每一次应该独立思考的时候,不选择随波逐流,应该全力以赴的时候,不选择尽力而为,不辜负每一秒存在的意义。

   转载和爬虫请注明原文链接http://www.cnblogs.com/tdws/p/5645075.html,博客园 蜗牛 2016年6月27日。

目录

第01章 异步编程介绍

第02章 为什么使用异步编程

第03章 手动编写异步代码

第04章 编写Async方法

第05章 Await究竟做了什么

第06章 以Task为基础的异步模式

第07章 异步代码的一些工具

第08章 哪个线程在运行你的代码

第09章 异步编程中的异常

第10章 并行使用异步编程

第11章 单元测试你的异步代码

第12章 ASP.NET应用中的异步编程

第13章 WinRT应用中的异步编程

第14章 编译器在底层为你的异步做了什么

第15章 异步代码的性能

编写Async方法

  现在我们已经知道异步代码有多棒了,但是它到底难写吗?是时候来看一看C#5.0的Async功能了。正如我们之前在第三章所看到的,一个async方法允许包含await关键字。

private asyncvoid DumpWebPageAsync(string uri)
{
WebClient webClient = new WebClient();
string page = awaitwebClient.DownloadStringTaskAsync(uri);
Console.WriteLine(page);
}

  由于await关键字在此方法中,到await这里就不再继续向下执行,直到下载结束恢复处理。这种处理使此方法异步,在本章,我们将会探索这样的异步方法。

 
将示例代码转换为Async的(第二章最后一个示例)

  我们现在将之前那个示例转换成Async的。如果可以,打开原版的代码,在你向下阅读前,尝试将它转换成async和await的方法。

  最重要的方法是AddFavicon,即,将下载后的icons添加到UI界面上的方法。我们想把它编程异步的,这样UI线程在下载期间就有空闲去相应用户的操作。第一步要做的就是添加async关键字到方法上。它和static关键字在一样的签名位置。

  然后我们需要使用await等待下载。await在C#语法中扮演者医院运算符的角色,就像‘!’或者‘(type)转换操作符’。他被放置在一个表达式的左侧,意于异步的等待表达式。

  最后,调用DownloadData方法必须替换成调用异步版本DownloadDataAsync。

         Async方法不是自动做到异步的。Async方法仅仅是将调用(消耗consume)其它异步方法更加容易。他们同步地运行着,一直到调用异步方法和await它。当他们做这样的事情时,必须使自身变得异步。有时,一个async方法不await任何事情。

private asyncvoid AddAFavicon(string domain)
{
WebClient webClient = new WebClient();
byte[] bytes = awaitwebClient.DownloadDataTaskAsync("http://" + domain + "/
favicon.ico");
Image imageControl = MakeImageControl(bytes);
m_WrapPanel.Children.Add(imageControl);
}

  比较一下这种方式和之前章节所介绍的版本。这看起来更像同步代码的样子。没有任何额外的方法,只在相同结构下有一点额外的代码。然而,他的行为和我们在上一章中的其中一小节(点击跳转)所写的版本很相像。

 
Task和Await

  让我们来分解一下我们写的await吧。下面是WebClient.DownloadStringTaskAsync方法。

Task<string> DownloadStringTaskAsync(string address)

  它的返回类型是Task<string>。就像我在介绍Task这一小节的介绍,Task代表一个执行中的操作。并且它的子类Task<T>代表着一个在将来某一时刻返回T类型的结果的操作。你可以认为Task<T>承诺返回T类型的值在这个耗时操作之后。

  Task和Task<T>都可以代表异步操作,并且都有能力在操作完成后进行回调。在手动实现的异步方式中,你使用ContinueWith方法,传递一个委托,让代码在耗时操作结束后继续下一步操作。await使用相同的方式执行你的剩余的代码(也就是await之后的代码)。

  如果你对Task<T>运用await,他成为了一个await expression,并且整个表达式都拥有T类型。这意味着你可以等待一个变量的结果,并且可以在剩余的后半部分方法中使用,就像我们在例子中所看到的。然而当你await一个非泛型Task时,它保持await状态,但不能被分配给任何东西,就像调用一个void方法。这意味着,作为一个Task不承诺返回任何值,他仅仅表示操作本身。

await smtpClient.SendMailAsync(mailMessage);

  没有什么可以把我们await表达式内部分开,所以我们可以直接的访问Task,或者在等待中做一些其他事情。具体看下代码和注释你就明白了。

Task<string> myTask = webClient.DownloadStringTaskAsync(uri);
// Do something here
string page = await myTask;

  完全理解它带来的启示很重要。DownloadStringTaskAsync方法在第一行执行,他开始在当前线程异步的执行,并且一旦开始了下载,它返回一个Task<string>(对照上面的代码理解),依然在当前线程。只是在后来我们await Task<string>时,编译器做了一些特别的事情。如果你把await写在和调用异步方法在一行代码里它一直是正确的。译者解释:也就是说如果调用await方法,在执行await内部操作的时候,这个线程是当前线程。执行await后的操作,可能是当前线程来处理后面的代码,也可能是新的线程来处理后面的代码。换种方式说,await所等待的方法,被当前线程来执行,但是执行时候立马回收到线程池,下一步操作随机选择一个线程来执行。因此我认为所有认为await会开新线程的说法是错误的。再强调一次,await后之所以会出现新的线程,是因为执行await内部的线程被回收到池子中,从线程池中再取出一个来执行下面的代码。await根本就没有开启线程的功能。

   一旦调用DownloadStringTaskAsync发生,耗时操作开始执行,这同时给了我们一个很简单的方法来执行多个异步操作。我们可以开始多个操作,保持Tasks,然后await他们。

Task<string> firstTask = webClient1.DownloadStringTaskAsync("http://oreilly.com");
Task<string> secondTask = webClient2.DownloadStringTaskAsync("http://simple-talk.com");
string firstPage = await firstTask;
string secondPage = await secondTask;

         等待多个Task是一种危险的方式,也许他们会抛出异常。如果两个操作抛出一个异常,第一个await将会传播它的异常,这意味着secondTask永远不会被等待。它的异常可能不会被注意到,还取决于.NET版本和设置,也许会丢失或者在另一个非预期线程中抛出,还可能终止该进程。我们将会在第七章讲到更好的方式去处理。

 
Async方法返回类型

  标记为async的方法有三种返回类型:

  ·void

  ·Task

  ·Task<T>

  没有其他允许的返回类型,因为通常再返回时方法都没执行结束。通常情况下,异步方法将会await一个耗时操作,意思是方法将会迅速返回,但是却在未来实现结果。也意味着,在方法返回时没有明确的结果,而是迟一些才会变得可用。

         我会展示方法返回值之间的区别—例如,Task<string>—这个返回类型,即编程人员打算返回给调用者的,在此情况下是string类型。通常,在非异步的方法中,返回类型和结果类型是一样的。但他们之间这样的不同对async方法很重要。

  很明显void返回类型是很合理的选择在异步编程情况中。一个async void方法是一个“触发并忘记”的异步操作。调用者不能等待任何返回结果,并且不能知道操作什么时候结束或者是否成功。当你确定你不需要知道操作何时结束或者是否成功时,你应该使用void。async void最常见的应用场景是在async代码和其他代码的边界情况,比如UI事件处理必须返回void。

  返回Task的异步方法允许调用者等待操作结束的结果,并且传递在异步代码执行期间的异常。当我们不需要任何返回类值时,一个async Task方法比async void方法更好,因为他允许调用者使用await去等待,并且处理异常更容易。(前面已经说到void最适合的情况)。

  最后,返回Task<T>的异步方法,像Task<string>,通常用于异步操作需要返回值的时候。

 
异步,方法签名,和接口

  async关键字出现在方法的声明上,就像public和static一样。尽管如此,async不能用于方法的签名,无论是重写方法,实现接口还是被调时。

  async关键字唯一的影响是在他所应用的方法内部编译,而不像其他关键字,决定其如何与外界交互。正因如此,在关于重写方法,定义接口的规则上完全不被理会。

class BaseClass
{
     public virtual async Task<int> AlexsMethod()
    {
     ...
    }
}
class SubClass : BaseClass
{
    // This overrides AlexsMethod above
            public override Task<int> AlexsMethod()
    {
        ...
    }
}

  接口不能使用async定义,很简单,因为没必要。如果一个借口需要方法返回Task,在实现时可以使用async,但是用不用还是方法自己的事儿。接口不需要特别声明出是否要异步。

 
Async方法的return Statement

   Return Statement在异步方法中有着不同的行为。想想在普通的非异步方法,使用return statement依赖于方法的返回类型。

  void方法

  return statement只需要return;,并且是可选择。(不写也行)

  返回一个T类型的方法

  return必须有一个T类型的表达式,比如5+x,并且必须出现在方法所有路径的最后。

  在一个标记为async的方法中,不同的情况也有不同的规则

  void方法和返回Task的方法

  return statement只需要return;,并且是可选择的。(不写也行)

  返回Task<T>的方法

  return必须返回一个T的表达式并且要在所有返回路径的最后。

  在异步方法中,方法的返回类型和表达式类型有所不同。编译器转换可以被认为是将你的结果值包裹起来,在返回给调用者之前。当然,事实上Task>T<立即被创建,并且一旦在你的耗时操作结束后,将你的值“填充”上。译者:像前几章讲的一样,异步方法立即返回被“包裹”的值,在执行结束后,填充值。

 
异步方法具有传染性

  正如我们所见,最好的使用由异步返回的Task的方式是在异步方法中await它。当你这样做时,你的方法通常也返回Task。为了享受异步风格的优势,你调用方法的代码必须不是阻塞地等待你的Task结束,并且这样的话,你的调用者很可能也在await你。

  下面示例是一个我曾经写过的方法,用于读取一个网页中有多少个字符,并且异步的返回它们。

private async Task<int> GetPageSizeAsync(string url)
{
    WebClient webClient = new WebClient();
    string page = await webClient.DownloadStringTaskAsync(url);
    return page.Length;
}

  To use it, I need to write another async method, which returns its result asynchronously为了使用它,我需要写另一个异步的返回自己结果的异步方法,就像这样:

private async Task<string> FindLargestWebPage(string[] urls)
{
     string largest = null;
     int largestSize = 0;
     foreach (string url in urls)
     {
          int size = await GetPageSizeAsync(url);
         if (size > largestSize)
         {
            size = largestSize;
            largest = url;
         }
      }
       return largest;
}

   在这种方式下,我们不用写异步的方法链,只是每次await就好。Async是一个传染性的编程模型,他可以很容易就弥漫到整个代码体系。但是我认为这正是由于async方法如此容易的书写,这完全没有问题。

 
异步匿名委托和Lambdas

   普通的命名方法可以异步,并且有两种匿名方法一样可以异步。语法和正常的方法也很像。下面是如何使用异步匿名委托的示例:

Func<Task<int>> getNumberAsync = async delegate { return 3; };

   下面是async lambda:

Func<Task<string>> getWordAsync = async () => "hello";

   和普通的异步代码规则没什么不一样。你可以用他们来保持代码清晰整洁,捕捉闭合,和非异步方法以完全相同的形式书写。

写在最后

  最近好迷茫,可能有点太急躁,总觉得高不成低不就,拼命地想越走越高,又看不到自己明显的进步。痛苦。

下一章节将介绍   await究竟做了什么。

以上是关于异步编程系列第04章 编写Async方法的主要内容,如果未能解决你的问题,请参考以下文章

C#与C++的发展历程第三 - C#5.0异步编程巅峰

超简单的Python教程系列——第14篇:异步

C#中await/async闲说

JavaScript 工作原理之四-事件循环及异步编程的出现和 5 种更好的 async/await 编程方式(译)

任务Task系列之异步编程(async and await)

async 和 await 之异步编程的学习