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

时间:2021-12-10 01:05:44

p {
display: block;
margin: 3px 0 0 0;
}
-->

写在前面

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

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

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

目录
编写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 = ;
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 ; };

下面是async lambda:

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

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

写在最后

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

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