异步编程系列第03章 自己写异步代码
在学异步,有位园友推荐了《async in C#5.0》,没找到中文版,恰巧也想提高下英文,用我拙劣的英文翻译一些重要的部分,纯属娱乐,简单分享,保持学习,谨记谦虚。
如果你觉得这件事儿没意义翻译的又差,尽情的踩吧。如果你觉得值得鼓励,感谢留下你的赞,愿爱技术的园友们在今后每一次应该猛烈突破的时候,不选择知难而退。在每一次应该独立思考的时候,不选择随波逐流,应该全力以赴的时候,不选择尽力而为,不辜负每一秒存在的意义。
转载和爬虫请注明原文链接http://www.cnblogs.com/tdws/p/5628538.html,博客园 蜗牛 2016年6月27日。
在本章,我们将会讨论一些关于不使用C#5.0关键字async的异步编程。这种方式虽然已经是过去的技术,也许你不会再使用,但这对于你理解异步编程表象背后发生了什么事情是很重要的。也因为这一点,我将会很快的讲述示例,仅仅着重揭示出对你理解有帮助的地方。
正如我之前提到的,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属性时,将会重新抛出异常,所以在代码中我们可以相应的处理异常。相反,他们可能在某个错误地方停止或者根本不去处理。
任务并行实在.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分支下(上面列出的站点下)提供了一个解决方案。其他更有效的解决方案也是有可能的。