《Async in C# 5.0》第六章 基于Task的异步模式

本文详细介绍了C# 5.0中基于Task的异步模式(TAP),包括TAP的设计原则,如何使用Task进行密集型计算,创建可控的Task,与非TAP异步模式的交互,以及冷启动任务和热启动任务的概念。TAP通过Task提供了简洁的异步编程方式,使得处理耗时操作更加灵活和高效。

基于Task的异步模式(TAP)是微软推荐的一系列使用Task类型编写异步API的方式。微软的并行编程小组的成员Stephen Toub写了一篇文章,其中有一些很棒的例子,值得一读。

使用async关键字来产生遵循此模式的方法,需要使用await进行调用。通常来说,以手动的方式使用Task类型很有帮助。在本章中,我将介绍这个模式,以及使用它的一些技巧。

TAP约定了什么

假定我们已经知道了如何来设计良好的C#同步方法签名:

  • 应该包含较少的参数,或者不使用参数。尽量避免使用ref和out参数。
  • 如果需要,应该包含返回类型,并且返回类型要能够真实表达方法需要的返回结果,而不是像一些C++方法那样,返回一个标识成功或者失败的值。
  • 应该使用可以明确阐明方法行为的名字,无需使用额外的注释。
  • 通常情况或者预期的失败都应该是返回类型的一种情况,但是非预期的错误应该抛异常

下面是一个设计得比较好的同步方法,它位于Dns类中

public static IPHostEntry GetHostEntry(string hostNameOrAddress)

基于你已有的同步方法的技巧,TAP针对异步方法给予了类似的指导:

  • 与同步方法相对应的异步方法应该包含相同的参数。永远不要使用ref和out参数。
  • 方法应该返回Task或者Task<T>,具体返回哪一个应该取决于异步方法是否含有返回类型。任务应该在将来的某一时刻结束,同时提供方法的结果值。
  • 应该命名为NameAsync,其中Name是对应的同步方法的名称。
  • 由方法使用错误导致的异常可能会直接从方法中抛出。任何其它的异常应该被放在Task中。

下面是一个设计得比较好的异步方法:

public static Task<IPHostEntry> GetHostEntryAsync(string hostNameOrAddress)

这看起来无需多讲,但是正如我们在第3章 .NET中采用的异步模式 所讲的那样,TAP是.NET framework中正式使用的异步模式。我敢说其他人肯定曾经使用过无数不正规的方式来书写异步代码。

TAP的核心思想是:针对异步方法会返回一个Task,它会确保耗时的操作在将来能够结束。如果不采用这种方式,之前说的异步模式要么需要在方法上添加额外的参数,要么需要在接口上添加额外的方法或者事件来支持回调机制。Task包括了支持回调所需的所有“基础设施”,同时使用Task还能保证代码的整洁性(因为它不引入过多的细节)。

另外的一个好处是,因为Task已经具备了异步回调的机制,这样就不需要在进行异步调用时到处重复编写处理回调的方法。反过来,这意味着这种机制可以提供更复杂和更强大的能力,使得可以像回调的方式那样处理诸如恢复上下文(包括同步上下文)等操作。它也提供了一组API来处理异步操作,使得像async这样的编译器功能合理化,如果使用其它模式则无法使其合理化。

使用Task进行密集型计算操作

有时,一个耗时操作并不一定会访问网络或者磁盘——它之所以需要较长时间,可能是因为它需要大量的处理器时间来完成复杂的计算。当然了,我们不能指望在不占用线程的情况下就做到这一点。在带用户界面的程序里,我们仍然希望避免界面失去响应。为了解决这个问题,我们不得不“释放”UI线程去处理其它的事件,然后使用另一个线程来处理耗时的计算。

Task提供了一种简单的方式来解决这个问题,你可以使用await结合Task在计算任务完成时去更新UI。

Task t = Task.Run(() => MyLongComputation(a, b));

Task.Run使用线程池ThreadPool中的线程去执行传递给它的delegate。在本例中,我使用了Lambda表达式,因为可以很容易地将局部变量传递给计算任务。作为结果的Task会立即启动,我们可以使用await去等待其结果。

await Task.Run(() => MyLongComputation(a, b));

如果想在后台线程上执行任务,这是一种非常简单的方式。

如果你需要更好地控制在哪个线程进行计算或如何进行排队,Task有一个类型为TaskFactory的静态属性Factory,Factory属性的StartNew方法有很多重载方法,这些方法可以控制计算任务的执行情况。

Task t = Task.Factory.StartNew(() => MyLongComputation(a, b),
                                cancellationToken,
                                TaskCreationOptions.LongRunning,
                                taskScheduler);

如果你正在编写包括很多密集型计算方法的类库,你也许很想给你的方法提供异步版本的方法,你可能会通过调用Task.Run来在后台线程上执行工作。但这不是一个好办法,因为你的API的调用者比你更了解应用程序对于线程的需求。例如,在web应用程序中,使用线程池没有什么好处,唯一应该优化的则是线程的总数。Task.Run是一种非常简单的调用方式,因为你的API的调用者完全可以在他需要的时候自行去调用。

创建一个可以被“随意控制”的Task(puppet task)

TAP非常易于使用,因此你很自然地会想到:你对外提供的所有API都支持TAP。当你使用他人提供的TAP API时,我们已经知道了该如何去“使用”它——使用async方法。但是,当耗时操作并未以TAP API的方式提供给我们时该怎么办呢?也许这些API使用了其他的异步方式。也许你并不想调用API,而只是以完全手动的方式去异步地做一些事情而已。

这时可以使用TaskCompletionSource<T>。通过它,你可以创建一个由你“随意控制”的Task对象。你可以在任何你希望的时刻让Task完成任务,同样你也可以通过向其传递exception来使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;
}

注意这个方法并未标注为aysnc;我们正在手动创建一个Task对象,因此我们不希望由编译器为我们生成一个Task。TaskCompletionSource<bool>创建了Task对象,并作为一个属性(属性名是Task)供我们使用。我们可以在后面使用TaskCompletionSource中的SetResult方法来标记Task完成任务。

因为我们遵循了TAP,所以这个方法的调用者只需要使用await关键字等待用户授权。方法的调用非常整洁。

if (await GetUserPermission())
{ ...

一个让人不爽的地方是,没有非泛型版本的TaskCompletionSource<T>。不过这不要紧,因为Task<T>是Task类的子类,因此你可以在任何需要Task的地方使用Task<T>。反过来说,这意味着你可以使用TaskCompletionSource<T>,并且由TaskCompletionSource<T>中的Task属性返回的Task<T>类型也是一个完全有效的Task。我个人倾向于使用TaskCompletionSource<object>并且调用SetResult(null)去标记任务完成,通过这种方式,你就可以基于泛型版本来生成一个非泛型的TaskCompletionSource。

与之前的异步模式进行交互

.NET团队在框架中针对所有的重要异步API都创建了TAP版本的API。一旦你需要与既存的异步代码进行交互,了解如何从非TAP的异步代码创建TAP方法也很有意思。如何使用TaskCompletionSource<T> 是一个很有意思的例子。

让我们回到之前使用的DNS查找的例子。在.NET 4.0中,DNS查找的异步版本使用了IAsyncResult异步模式——包括一个Begin方法和一个End方法。

IAsyncResult BeginGetHostEntry(
    string hostNameOrAddress,
    AsyncCallback requestCallback,
    object stateObject)

IPHostEntry EndGetHostEntry(IAsyncResult asyncResult)

通常,你会使用lambda表达式作为回调的方式来使用这个API,并且在表达式内调用End方法。正如下面的代码所示,但是下面代码的回调中没有做实质性的工作,我们只是使用TaskCompletionSource<T>来标记任务结束。

public static Task<IPHostEntry> GetHostEntryAsync(string hostNameOrAddress)
{
    TaskCompletionSource<IPHostEntry> tcs = new TaskCompletionSource<IPHostEntry>();
    Dns.BeginGetHostEntry(hostNameOrAddress, asyncResult =>
    {
        try
        {
            IPHostEntry result = Dns.EndGetHostEntry(tcs.SetResult(result);
        }
        catch (Exception e)
        {
            tcs.SetException(e);
        }
    }, null);
    
    return tcs.Task;
}

由于可能发生异常,所以上面的代码稍显复杂。如果DNS解析失败,当我们调用EndGetHostEntry时就会抛出异常。这就是为何IAsyncResult模式使用了“曲折迂回的”End方法,而不是直接在回调中返回结果。当抛出异常时,我们应该将其放到TaskCompletionSource<T>中,这样调用者就可以根据TAP的方式得到这个异常。

事实上,.NET Framework中有很多遵循了这种模式的异步API,.NET Framework团队创造了工具类方法来将异步API转化为TAP版本,我们也可以使用这种方式

Task t = Task<IPHostEntry>.Factory.FromAsync<string>(
    Dns.BeginGetHostEntry,
    Dns.EndGetHostEntry,
    hostNameOrAddress,
    null);

这种方式将Begin和End方法作为委托,并且使用了一个与我们上面非常相似的机制——也许它处理得比我们更高效。

冷启动任务与热启动任务(Cold and Hot Tasks)

当TPL(Task Parallel Library)最初在.NET 4.0引入Task类型时,它提出了冷启动任务的概念——任务需要被启动才能运行。与之对应的是热启动任务——任务无需启动直接运行。目前为止,我们只需要处理热启动任务。

TAP指定所有的Task在从一个方法返回前必须是热启动型的。幸运的是,我们讲过的所有技术创建的Task都是热启动的。例外的是TaskCompletionSource<T>,它没有冷启动和热启动Task的概念,你只需要确保在合适的时机结束Task。

前期工作

我们已经知道,当调用一个TAP异步方法时,方法是运行在当前线程上,就像调用其他的方法一样。区别是:TAP方法在返回前很可能并未真正完成工作,它会先快速返回一个Task对象,当真正的工作完成时这个Task才会结束。

话虽如此,方法中的一些代码会在当前线程以同步的方式执行。在标记为async的方法中,同步运行的代码从方法起始位置一直到第一个await处,就像上一章的async方法是同步的,除非需要了才会变为异步部分所讲的那样。

TAP推荐:通过TAP方法执行的同步工作应该尽可能少。你可以通过检测参数的有效性,扫描缓存来避免长时间进行某种操作,但是你不应该(以同步的方式)运行缓慢的计算。对于混合方法——既进行计算,紧接着又进行网络访问或者执行其他类似操作——你应该使用Task.Run将计算工作放到背景线程。想象一下在上传图片到网站前,需要先将图片尺寸缩小以保存带宽

Image resized = await Task.Run(() => ResizeImage(originalImage));
await UploadImage(resized);

在带UI的程序中这样做很重要,但是对于web程序则没有实际的好处。不过,当我们看见一个TAP模式的方法时,我们总会期望它快速返回。如果你以同步的方式来缩小图片尺寸,并且这个过程又很慢时,如果其他人在带UI程序中使用了你的代码时,应该会收到意外惊喜(译者注:UI会意想不到地hang住)。

<完>

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值