usercount

异步编程系列06章 以Task为基础的异步模式(TAP)

写在前面

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

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

   转载和爬虫请注明原文链接http://www.cnblogs.com/tdws/p/5679001.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章 异步代码的性能

以Task为基础的异步模式

  基于Task的异步编程模式(TAP)是Microsoft为.Net平台下使用Task进行编程所提供的一组建议和文档—地址(译者:后续也会翻译此文档,写的确实不错):http://www.microsoft.com/en-gb/download/details.aspx?id=19957。微软并行编程团队的Stephen Toub在文档中提供了好的例子,很值得一读。

  这种模式提供了可以被await消耗(调用)方法的APIs,并且当使用async关键字编写遵守这种模式的方法时,手写Task通常很有用。在本章,我将介绍如何使用这种模式和技术。

TAP具体指什么?

  我假设我们已经知道如何使用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一样合理,其他模式则达不到这种效果。

使用Task来做计算密集型的操作

  有时,一个耗时操作既不做网络请求也不访问磁盘;他只是在一个需要很多处理器时间的复杂运算上耗费了时间。当然,我们不能指望做到这一点像网络请求一样不占用线程。但是在程序的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以来调用吧。

创建一个可控的Task

  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方法作为委托,并且使用的机制和我们以前的很像。但它可能比我们简单的方法更高效。

冷Task和热Task

  在.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中,如果你的图片调整非常缓慢,将会是一个“惊喜”

写在最后

  这周更新的有点慢。需要英文原著的可以私信或留言。如果有错误,希望能指出。下一篇将介绍:异步代码的一些工具方法

posted @ 2016-07-17 17:23  坦荡  阅读(3462)  评论(3编辑  收藏  举报