usercount

异步编程系列第03章 自己写异步代码

写在前面

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

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

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

目录

第01章 异步编程介绍

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

第03章 手动编写异步代码

    .NET中的一些异步模式
    最简单的异步模式
    关于Task的介绍
    手动编写异步代码的问题
    使用手写异步代码转换示例(第二章最后一个示例)

第04章 编写Async方法

第05章 Await究竟做了什么

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

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

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

第09章 异步编程中的异常

第10章 并行使用异步编程

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

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

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

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

第15章 异步代码的性能

手动编写异步代码

  在本章,我们将会讨论一些关于不使用C#5.0关键字async的异步编程。这种方式虽然已经是过去的技术,也许你不会再使用,但这对于你理解异步编程表象背后发生了什么事情是很重要的。也因为这一点,我将会很快的讲述示例,仅仅着重揭示出对你理解有帮助的地方。

 
.NET中的一些异步模式

  正如我之前提到的,Silverlight只提供了像web访问的异步版本API。这里有一个例子,你可以下载一个网页,并显示它:

private void DumpWebPage(Uri uri)
{
WebClient webClient = new WebClient();
webClient.DownloadStringCompleted += OnDownloadStringCompleted;
webClient.DownloadStringAsync(uri);
}
private void OnDownloadStringCompleted(object sender,
DownloadStringCompletedEventArgs eventArgs)
{
m_TextBlock.Text = eventArgs.Result;
}

  这种API是基于事件的异步模式(EAP)。这个想法是想替代单线程方法去下载网页,即阻塞型代码会一直等到下载结束再调用一个方法或触发一个事件。这个方法看起来和同步代码一样,除了无返回类型。这个事件也有一个特别的eventArgs类型,它包含值检索。

  我们在调用这个方法前注册了事件。该方法立即返回,当然这是因为它是异步代码。然后在将来的某个时刻触发。这种模式显然很复杂,不仅仅是因为你要将它分成像例子一样的两个方法。最重要的是,你注册了一个时间增加了复杂性。如果说我还要用相同的WebClient实例处理其他需求,那么你也许不希望这个时间依然被附加着并且再次执行一遍。

  在.NET功能中另一个异步模式设计IAsyncResult接口。其中一个例子就是DNS查找主机名的IP地址,BeginGetHoseAddress。这种设计要求两个方法,一个是开始执行的BeginMethodName,另一个是执行结束EndMethodName,即你的回调方法。

private void LookupHostName()
{
object unrelatedObject = "hello";
Dns.BeginGetHostAddresses("oreilly.com", OnHostNameResolved, unrelatedObject);
}
private void OnHostNameResolved(IAsyncResult ar)
{
object unrelatedObject = ar.AsyncState;
IPAddress[] addresses = Dns.EndGetHostAddresses(ar);
// Do something with addresses
...
}

  至少这种方式不会遭受残留注册事件的影响,然而这也额外的对API增加了复杂性。有两个方法而不是一个,我觉得很不自然。

  这两种异步模式都需要你分为两个方法来书写。IAsyncResult模式要你从第一个方法中向第二个方法传递某些参数,就像我传递了string类型的"hello"。但是这种方式很复杂,即使你不需要这个参数,还是不得不传递它,并且迫使你转换为object类型。

 
最简单的异步模式

  可以说下面这段代码拥有异步行为,即使不使用async关键字,也不用向方法传递委托:

void GetHostAddress(string hostName, Action<IPAddress> callback)

  我发现这种方式比其他方式更加易用。

private void LookupHostName()
{
GetHostAddress("oreilly.com", OnHostNameResolved);
}
private void OnHostNameResolved(IPAddress address)
{
// Do something with address
...
}

  不同于两个方法的模式,像我以前提到的,使用异步方法或者用lambda表达式做回调。它拥有重要的好处就是可以在第一个方法中访问变量。

private void LookupHostName()
{
int aUsefulVariable = 3;
GetHostAddress("oreilly.com", address =>
{
// Do something with address and aUsefulVariable
...
});
}

  这个Lambda有一点难以阅读,并且通常如果你使用多重的异步编程,你将需要很多Lambda表达式相互嵌套,你的代码将会很快变得犬牙交错和难以处理。

  这种简单方法的缺点在于他们不再对调用者抛出异常。在之前.NET异步编程中,调用EndMethodName或者得到Result属性时,将会重新抛出异常,所以在代码中我们可以相应的处理异常。相反,他们可能在某个错误地方停止或者根本不去处理。

 
关于Task的介绍

  任务并行实在.NET Framework4.0版本中推出的。其最重要的地方是Task类,即代表一个正在执行的操作。 泛型版本的Task<T>, 当操作完成时返回类型为T的值。

   在C#5.0 async功能上我们大量的使用了Task,我们将会稍后讨论。然而即使没有async,你依然可以使用Task,尤其是使用Task<T>来异步编程。这样做就行,你开始一个返回Task<T>的操作,然后使用ContinueWith方法注册你的回掉方法。

private void LookupHostName()
{
Task<IPAddress[]> ipAddressesPromise = Dns.GetHostAddressesAsync("oreilly.com");
ipAddressesPromise.ContinueWith(_ =>
{
IPAddress[] ipAddresses = ipAddressesPromise.Result;
// Do something with address
...
});
}

  Task的优点就像这个DNS只需要一个方法,使API更加整洁。所有调用异步行为相关的逻辑都可在Task类当中,所以它不需要在每一个方法里都进行复制。这个逻辑可以做很多重要的事儿,比如处理异常和同步上下文(SynchronizationContexts)。这些,我们将会在第八章讨论,对于在一个特定线程上执行callback很有用处(比如UI线程)。

  最重要的是,Task给我们提供一种使用异步的相对抽象的操作方式。我们可以利用这种组合型去编写我们的工具,即在很多需要使用Task的情况下提供给一些有用的行为。我们将会看到很多相关的工具组件(utilities)在第七章当中。

 
手动编写异步代码的问题

   正如我们看到的,我们有很多方式来实现异步编程。有一些方式比其他方式整洁易懂易用,但是也希望你已经看出他们共有的缺陷。你打算写的程序不得不分为两个方法:实际的方法和回调方法。还有使用异步方法或嵌套多次lambda表达式作为回调,使你的代码一环套一环难以理解。

  实际上这里还有另一个问题。我们已经说过调用一次异步方法的情况,但是当你需要多个异步时会发生什么呢?更糟糕的是,如果弄需要在循环中调用异步又会发生什么呢?你为一个方式是使用递归方法,这又比普通的循环难以阅读多了。

private void LookupHostNames(string[] hostNames)
{
LookUpHostNamesHelper(hostNames, 0);
}
private static void LookUpHostNamesHelper(string[] hostNames, int i) { Task<IPAddress[]> ipAddressesPromise = Dns.GetHostAddressesAsync(hostNames[i]); ipAddressesPromise.ContinueWith(_ => { IPAddress[] ipAddresses = ipAddressesPromise.Result; // Do something with address ... if (i + 1 < hostNames.Length) { LookUpHostNamesHelper(hostNames, i + 1); } }); }

  哇!

  在这些异步编程方式中,引发的另一个问题就是需要消耗大量代码。如果你写一些异步代码,期望在其他地方使用,你不得不提供API,如果API混乱或者忘记当时的初衷不能理解的话,将会事半功倍。异步代码是会“传染”的,因此不仅你需要异步API,还影响调用者和调用者的调用者,知道整个程序乱成一团。

 
使用手写异步代码转换示例(第二章最后一个示例)

  再来谈谈第二章最后一个示例,我们讨论了一个会因从网站下载icons,造成UI线程阻塞,并导致出现应用程序未响应的WPF UI app。现在我们将会看到,将它转化成手写的异步代码。

  第首先要做的就是找到一个异步API的版本,我用(WebClient。下载文件)。正如我们已经看到的,WebClient方法使用基于事件的异步方式(EAP),所以我们可以在开始下载之前注册一个事件作为回调方法。

private void AddAFavicon(string domain)
{
WebClient webClient = new WebClient();
webClient.DownloadDataCompleted += OnWebClientOnDownloadDataCompleted;
webClient.DownloadDataAsync(new Uri("http://" + domain + "/favicon.ico"));
}
private void OnWebClientOnDownloadDataCompleted(object sender,
DownloadDataCompletedEventArgs args)
{
Image imageControl = MakeImageControl(args.Result);
m_WrapPanel.Children.Add(imageControl);
}

  当然,我们的真正属于一起的逻辑要被分成两个方法。我不喜欢使用Lambda来代替刚才的EAP,因为lambda会出现在真正开始下载前,我觉得这是不可读的。

  这个版本的示例也可以在线(https://bitbucket.org/alexdavies74/faviconbrowser)找到,(//译者注释:不运行此程序也没关系,主要是体会下思路就好)在manual分支。如果你运行它,不进界面可相应,图标也会逐一出现。正因此,我们也引入了一个bug,现在由于所有下载操作同时开始,icons的排序由其下载先后决定,而不是由我的先后请求来决定。如果你想检验自己是否理解手动编写异步代码,我建议你尝试着解决此bug。在orderedManual分支下(上面列出的站点下)提供了一个解决方案。其他更有效的解决方案也是有可能的。

写在后面
27号入职,花了三天的业余时间,坎坎坷坷的翻译了第三章。如果你对你有些许益处,不要吝啬你的赞,给个鼓励。不准确和需要补充的地方,也请前辈们不吝赐教,我将虚心改正。下一章将会介绍 “编写Async方法
posted @ 2016-06-29 23:05  坦荡  阅读(3164)  评论(11编辑  收藏  举报