异步编程系列06章 以Task为基础的异步模式(TAP)
在学异步,有位园友推荐了《async in C#5.0》,没找到中文版,恰巧也想提高下英文,用我拙劣的英文翻译一些重要的部分,纯属娱乐,简单分享,保持学习,谨记谦虚。
如果你觉得这件事儿没意义翻译的又差,尽情的踩吧。如果你觉得值得鼓励,感谢留下你的赞,愿爱技术的园友们在今后每一次应该猛烈突破的时候,不选择知难而退。在每一次应该独立思考的时候,不选择随波逐流,应该全力以赴的时候,不选择尽力而为,不辜负每一秒存在的意义。
转载和爬虫请注明原文链接http://www.cnblogs.com/tdws/p/5679001.html,博客园 蜗牛 2016年6月27日。
基于Task的异步编程模式(TAP)是Microsoft为.Net平台下使用Task进行编程所提供的一组建议和文档—地址(译者:后续也会翻译此文档,写的确实不错):http://www.microsoft.com/en-gb/download/details.aspx?id=19957。微软并行编程团队的Stephen Toub在文档中提供了好的例子,很值得一读。
这种模式提供了可以被await消耗(调用)方法的APIs,并且当使用async关键字编写遵守这种模式的方法时,手写Task通常很有用。在本章,我将介绍如何使用这种模式和技术。
我假设我们已经知道如何使用C#设计一个好的异步方法:
·它应该有尽量少的参数,甚至不要参数。如果可能的话一定要避免ref和out参数。
·如果有意义的话,他应该有一个返回类型,他能真正的表达方法代码的结果,而不是像C++那种成功标识。
·它应该有一个可以解释自己行为的命名,而不依赖于额外的符号或注释。
预期内的错误应该作为返回类型的一部分,而非预期内的则应抛出异常。
这里有一个DNS类下的,设计的不错的异步方法:
public static IPHostEntry GetHostEntry(string hostNameOrAddress)
TAP提供了设计异步方法相同的准则,基于你已经掌握的异步方法技能。如下:
·他应该和异步方法有相同的参数,ref和out参数一定要避免。
·他应该返回Task或者Task<T>,当然这取决你你的方法是否有返回类型。这个任务应该在将来的某个时刻完成,并提供结果。
·他应该被命名为NameAsync,Name等价于你表示作用的异步方法名字。
·由于方法运行中我们错误(预期外)的导致的异常应该被直接抛出。任何其他异常(预期内)应该由Task来带出。
下面是一个好的TAP方法设计:
public static Task<IPHostEntry> GetHostEntryAsync(string hostNameOrAddress)
这一切似乎非常明显,但是像我们在之前讲过的.NET异步编程的几种模式http://www.cnblogs.com/tdws/p/5628538.html#one,这是第三种正是在.NET框架下应用的异步模式,并且我确定有无数的非正式的方式来写异步代码。
TAP的关键理念是异步方法返回Task,即封装了将来完成耗时操作的结果。如果没有这个理念,我们过去的 异步模式不是要给方法增加额外参数,就是要增加额外方法或者事件来支撑回调机制。Task可以包含任何回调所需要的基础内容,而不需要以往杂乱的细节来污染你的方法,造成阅读和书写困难。
额外的好处是,由于异步回调的机制现在在Task中,在异步调用时你不需要到处复制和准备回调方法。反过来这意味着这种机制能够承担更加复杂强大的任务,使其能够做一些可行的事儿像恢复上下文,包括同步上下文。它也提供了一个通用的API用于处理异步操作,使编译器功能像async一样合理,其他模式则达不到这种效果。
有时,一个耗时操作既不做网络请求也不访问磁盘;他只是在一个需要很多处理器时间的复杂运算上耗费了时间。当然,我们不能指望做到这一点像网络请求一样不占用线程。但是在程序的UI上,我们依然希望避免UI冻结造成不响应。为了解决这件事儿,我们不得不返回UI线程来处理其他事件,并且用一个不同的线程来做耗时计算。
Task提供了一种很简单的方法来做这件事,并且你可以使用await像其他异步一样,从而在计算完成是更新UI界面:(译者注释:Task.Run()可以开启一个新的后台线程。)
Task t = Task.Run(() => MyLongComputation(a, b));
Task.Run方法使用了ThreadPool中的一个线程来执行你给的委托。在这种情况下,我使用了一个lambda使其更加容易传递本地变量到计算当中。由此产生的Task立即开始,并且我们可以await这个Task:
await Task.Run(() => MyLongComputation(a, b));
这是一个在后台线程工作的很简单的方式。
比如你需要更多的控制,比如使用哪个线程执行计算或者如何排队。Task有一个静态的叫做TaskFactory类型的Factory的属性。它有一个StratNew方法可以控制你计算的执行:
Task t = Task.Factory.StartNew(() => MyLongComputation(a, b),
cancellationToken,
TaskCreationOptions.LongRunning,
taskScheduler);
如果你在编写一个包含大量计算密集型的方法的类库,你也许h忽视了去给你的方法提供一个异步版本,即可以通过调用Task.Run来开始工作在后台线程中的版本。这不是一个好主意,因为你的API调用者比你更了解应用程序的线程需求。举个例子。在web应用中,使用线程池没有好处;唯一应该优化的是线程总数。Task.Run是一个很简单的调用,所以如果需要的话就给调用者留下API以来调用吧。
TAP真的很容易被消费(调用),所以你可以在你所有的接口中很容易的提供TAP模式。我们已经知道在消费其他TAP API时如何做,也知道如何使用使用异步方法。但是当耗时操作没有可用的TAP API会怎样呢?也许这是一个使用其他异步模式的API,也许你没有在消费(调用)一个API而是在做一些完全手动的异步。
这里我们使用的工具是TaskCompletionSource<T>这是一种受你控制创建Task的方式。你可以使Task在任何你想要的时候完成,你也可以在任何地方给它一个异常让它失败。
我们来看看这个例子。假设你想通过下面这个方法封装一个提示展示给用户:
Task<bool> GetUserPermission()
这封装的是一个提示用户是否同意的自定义对话框。因为用户的许可在你程序里很多的地方需要,定义一个容易调用的方法是很重要的。这是一个很棒的地方来使用异步方法,因为你一定想释放UI线程来实现展示对话框。但是它也不接近传统的用于网络请求和其他耗时操作的异步方法。在这里,我们是等待用户,我们来看看这个方法的内部。
private Task<bool> GetUserPermission() { // Make a TaskCompletionSource so we can return a puppet Task TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); // Create the dialog ready PermissionDialog dialog = new PermissionDialog(); // When the user is finished with the dialog, complete the Task using SetResult dialog.Closed += delegate { tcs.SetResult(dialog.PermissionGranted); }; // Show the dialog dialog.Show(); // Return the puppet Task, which isn't completed yet return tcs.Task; }
注意这个方法没有被标记为async;我们手动的创建了一个Task,所以我们不希望编译器为我们创建一个。TaskCompletionSource<T>创建了这个Task,并且将它作为一个属性来返回,我们之后可以使用SetResult方法在TaskCompletionSource上使得该Task完成。
由于我们遵守了TAP,我们的调用者就可以使用await来等待用户的许可,这个调用很自然。
if (await GetUserPermission()) { ...
有一个烦恼就是TaskCompletionSource<T>没有一个非泛型版本。然而由于Task<T>是Task的父类,你可以在任意你需要Task的地方使用Task<T>。反过来意味着你可以使用一个TaskCompletionSource<T>,并且由一个Task作为属性所返回的Task<T>是完全有效的。我往往使用一个TaskCompletionSource<Object>并且调用SetResult(null)来完成它。你可以很容易个创建一个非泛型TaskCompletionSource如果你需要的话,可以基于一个泛型的(译者:把泛型的作为父类)。
.NET团队在框架的所有重要的异步编程API上创建了TAP模式的版本。但很有趣的是去理解如何把一个非TAP模式的异步代码构建成TAP的,在这样的情况下,你需要和已有的异步代码相互作用。下面有一个如何使用TaskCompletionSource<T>的有趣的例子。
我们来检查一下之前使用DNS例子的方法。在.NET4.0中,这个DNS方法使用的异步版本是IAsyncResult模式。这意味着它有Begin方法和End方法组成:
IAsyncResult BeginGetHostEntry(string hostNameOrAddress, AsyncCallback requestCallback, object stateObject) IPHostEntry EndGetHostEntry(IAsyncResult asyncResult)
通常情况,你也许消费(调用)这个API通过使用一个lambda作为回调,并且在其中我们要调用End方法。我们要在此做的很明确,值使用一个TaskCompletionSource<T>去完成一个Task。
public static Task<IPHostEntry> GetHostEntryAsync(string hostNameOrAddress) { TaskCompletionSource<IPHostEntry> tcs = new TaskCompletionSource<IPHostEntry>(); Dns.BeginGetHostEntry(hostNameOrAddress, asyncResult => { try { IPHostEntry result = Dns.EndGetHostEntry(asyncResult); tcs.SetResult(result); } catch (Exception e) { tcs.SetException(e); } }, null); return tcs.Task; }
这段代码被可能的异常变得更复杂。如果该DNS方法失败,在我们调用EndGetHostEntry方法时会抛出异常。这就是为什么IAsyncResult模式在End方法中使用了一个比仅仅传递结果到回调方法中更令人费解的系统。当异常被抛出时,我们应该把它装载到我们的TaskCompletionSource<T>当中,以便我们的调用者可以通过TAP的形式拿到异常。
实际上,我们有足够的API遵循这种模式,像.NET框架团队编写了一个工具方法,可以将它们转换成TAP版本,比如下面这样:
Task t = Task<IPHostEntry>.Factory.FromAsync<string>(Dns.BeginGetHostEntry, Dns.EndGetHostEntry, hostNameOrAddress, null);
它将Begin和End方法作为委托,并且使用的机制和我们以前的很像。但它可能比我们简单的方法更高效。
在.NET4.0中,任务并行库最初提成了Task类型,他有一个 cold Task的概念,这仍是需要开始的,与其相对的hot Task,则是已经在运行的。目前为止,我们仅处理了hot Task。
TAP明确指出所有Task在从方法返回前必须是hot的。幸运的是,我们之前所讲的所有技术中创建的Task都是hot Task。例外的是TaskCompletionSource<T>,它实际上没有什么cold和hot Task的概念。只需要确保在未来某个时刻完成可该Task。
我们已经知道当你调用一个TAP的异步方法,和其他任何方法一样,这个方法在当前线程中运行。不同点在于TAP方法在返回前没有真正的完成工作。他会立即返回一个Task,并且Task将会在实际工作结束后完成。
我们已经说过,一些代码将会在方法中同步的运行,并且在当前线程。在这种情况下的异步方法,至少代码可达,并且包括操作数,第一个await,正如我们在“异步方法直到被需要前是同步的”所讲到的。
TAP建议通过TAP方法所做的同步工作应尽可能最少数量。你可以检查参数是否有效,也可以通过扫描一个缓存来避免耗时操作,并且你也不应该在其中做一个缓慢的计算。混合的方法,即做一些运算,接着做一些网络请求或类似的事情是很好的办法,但是你应该使用Task.Run将该计算移到后台线程。想象一下常规的功能上传图片到网站上,但需要首先调整大小来节省带宽:
Image resized = await Task.Run(() => ResizeImage(originalImage)); await UploadImage(resized);
这在UI app上是很重要的,这对于web app没有什么特别的好处。当我们看见一个遵守TAP模式的方法,我们希望他迅速返回。任何使用你代码的人,并将其移动到UI app中,如果你的图片调整非常缓慢,将会是一个“惊喜”
这周更新的有点慢。需要英文原著的可以私信或留言。如果有错误,希望能指出。下一篇将介绍:异步代码的一些工具方法