《Async in C# 5.0》第七章 异步代码工具集

基于Task的异步模式(TAP),我们很容易创建处理Tasks的工具方法。所有的TAP方法都会返回一个Task,因此我们针对一个TAP方法写的所有特殊行为,都可以在其它方法上进行重用。在本章中,我们将探讨一些用于处理Task的工具方法,包括:

  • 那些看起来像TAP的方法,但是却具有一些特殊行为,并且自身不会被异步调用
  • 用来处理Tasks的组合器方法(Combinators),基于它会生成有用的新Tasks
  • 在异步操作期间可以取消操作、显示进度的工具

尽管已经有很多这类工具方法了,但是将来你可能需要定义类似的工具(.NET framework未提供时),所以看看如何自己实现这样的工具方法还是很必要的。

延迟一段时间

你想执行的最简单的耗时操作可能就是在一定的时间内什么也不做。这等同于同步世界中的 Thread.Sleep 。事实上,你可以使用 Thread.Sleep 结合Task.Run 来进行实现

cs 复制代码
await Task.Run(() => Thread.Sleep(100));

但这种方式是一种浪费。一个线程仅仅被用来阻塞一段时间,就是一种浪费。在.NET 中已经存在一种方式在不使用任何线程的情况下,在一段时间后调用你的代码------ System.Threading.Timer 类。更有效的方式是设置一个 Timer,然后使用 TaskCompletionSource 去创建Task,当 Timer 被触发时设置Task 为完成:

cs 复制代码
private static Task Delay(int millis)
{
    TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
    Timer timer = new Timer(_ => tcs.SetResult(null), null, millis, Timeout.Infinite);
    tcs.Task.ContinueWith(delegate { timer.Dispose(); });
    return tcs.Task;
}

当然了,在 framework 中已经提供了一个有用的小工具------ Task.Delay,并且 framework 提供的版本更强大、健壮,可能比我上面写得方法更高效。

等待一组Tasks

正如我们在第四章 Task和await 所见,通过依次调用异步操作,然后依次等待它们,很容易以并行方式运行多个异步操作。我们在 第九章 中将会发现:我们需要 await 每个启动的Task,这一点很重要,否则异常将会丢失。

这个问题的解决方案是使用 Task.WhenAll,它可以接收多个Tasks 然后生成一个聚合的 Task,这个聚合的Task将在所有输入的Task完成后才完成。下面是最简单版本的 WhenAll,它也包含针对Task<T> 的重载方法:

cs 复制代码
Task WhenAll(IEnumerable<Task> tasks)

使用 WhenAll 与你自己写代码等待多个 tasks的核心区别是: 当抛出异常时 WhenAll 能正确处理相关行为。基于这个原因,你应该一直使用 WhenAll。

泛型版本的 WhenAll 会返给你包含你提供的单个Task 的结果的数组。但这不是必须的,仅是为了使用方便,因为你仍然可以访问原始的Tasks,因此你可以使用它们的 Result 属性(因为它们必然已经执行完成)。

让我们修改 favicon browser 来作为示例。记住我们现在有一个调用 async void 方法的版本开始依次下载每一个图标。当下载结束时这个方法会把图标添加到窗体中。这种方式非常有效,因为所有的下载都是并行进行的,但存在两个问题:

  • 图标会以下载结束的先后顺序显示在窗体中
  • 因为每个图标都在自己的 async void 方法中下载,任何里面抛出的异常都被重新抛到UI线程,这种情况下处理异常会很困难

因此让我们进行重构,把遍历所有图标的方法变为async。这意味着我们可以把这些异步操作作为一个组。从这次重构之后的版本开始,我们将依次处理每个图标:

cs 复制代码
private async void GetButton_OnClick(object sender, RoutedEventArgs e)
{
    foreach (string domain in s_Domains)
    {
        Image image = await GetFavicon(domain);
        AddAFavicon(image);
    }
}

现在我们来修正代码,使它不但以并行的方式进行所有的下载工作,还能按照顺序显示图标。我们通过调用 GetFavicon 来开始所有的下载操作,并且将Tasks存放到列表中。

cs 复制代码
List<Task<Image>> tasks = new List<Task<Image>>();
foreach (string domain in s_Domains)
{
    tasks.Add(GetFavicon(domain));
}

如果你喜欢使用LINQ,这样写会更好

cs 复制代码
IEnumerable<Task<Image>> tasks = s_Domains.Select(GetFavicon);

// The IEnumerable from Select is lazy, so evaluate it to start the tasks
tasks = tasks.ToList();

一旦我们有了Tasks列表,就可以将它赋给 Task.WhenAll,它会返回一个 Task 对象,这个Task 会在所有下载任务都完成后完成,带着所有的结果

cs 复制代码
Task<Image[]> allTask = Task.WhenAll(tasks);

然后,我们要做的就是await allTask,使用它的返回结果

cs 复制代码
Image[] images = await allTask;
foreach (Image eachImage in images)
{
    AddAFavicon(eachImage);
}

如此这般,我们成功地以短短几行代码就写出了复杂的并行逻辑。最终的代码在whenAll 分支上可以获取到。

等待集合中的任何一个 Task

在使用多个 Tasks 时另一个可能需要的工具方法是等待第一个Task结束。可能的使用场景是:你正在从各种各样的资源中请求一个资源,不管哪个资源最先返回我们就使用哪个。

完成这个工作的工具方法是 Task.WhenAny。下面是一个泛型版本。同样,也有很多重载方法,但是下面这个很有意思。

cs 复制代码
Task<Task<T>> WhenAny(IEnumerable<Task<T>> tasks)

WhenAny 的方法签名比 WhenAll 稍微难理解一点, 但这自有其道理。当可能发生异常时,则需要小心使用 WhenAny。如果你想找出程序中发生的所有异常,你需要确保每一个 Task 都被 await了,否则异常可能会丢失。使用 WhenAny 并且简单地忘掉其它的 Tasks 等同于捕获了所有异常并进行忽略,这不是好的做法,往往会在后期显现为隐蔽的缺陷和无效状态。

WhenAny 的返回值是 Task<Task<T>>。这意味着当你await 它之后,你将得到一个 Task<T>,这是原来的Tasks 中最先完成的那个,因此当你得到它时它一定是已经完成了的。之所以返回Task 而不只是结果T,是因为这样你就可以知道到底原来Tasks中的哪个Task先完成了,从而可以取消或者await其余的Tasks。

cs 复制代码
Task<Task<Image>> anyTask = Task.WhenAny(tasks);
Task<Image> winner = await anyTask;
Image image = await winner; // This always completes synchronously

AddAFavicon(image);

foreach (Task<Image> eachTask in tasks)
{
    if (eachTask != winner)  // <-- 可以用来判断task
    {
       await eachTask;
    }
}

winner Task刚刚结束就用它更新UI是没有问题的,但当这个操作完成后,你应该像我代码所写的那样 await 所有剩下的 Tasks,这些额外的代码对你的程序并没有影响,但愿他们都能成功完成。但是如果剩下的 Tasks中有一个失败了,你需要找出它并修复这个问题。

创建自己的组合器(Combinators)

我们称 WhenAll 和 WhenAny 为异步组合器。尽管它们会返回Tasks,但他们自身却不是异步方法,而是以有用的方式组合了其它Tasks。如果需要,你也可以编写自己的组合器,这样你就会拥有一个包含可重用并行行为的工具包,为你所用。

让我们来写一个组合器作为例子。或许我们想在任何Task上添加超时机制。尽管我们可以很容易地从头编写这个功能,但我想用它作为使用 Delay 和 WhenAny 的良好示例。一般来说,组合器使用async进行实现是最容易的,就像在这个例子里一样,但是有时你也可以不这么做。

cs 复制代码
private static async Task<T> WithTimeout<T>(Task<T> task, int time)
{
    Task delayTask = Task.Delay(time);
    Task firstToFinish = await Task.WhenAny(task, delayTask);
    
    if (firstToFinish == delayTask)
    {
        // The delay finished first - deal with any exception
        task.ContinueWith(HandleException);
        throw new TimeoutException();
    }
    
    return await task; // If we reach here, the original task already finished
}

我的方式是创建一个使用了 Delay 方法的Task,它会在超过指定时间后结束。然后我使用 WhenAny 来控制 这个 delayTask 和原来的 Task,不管哪个先完成我都继续执行操作,或者是操作结束,或者是超时结束。然后,匹配看是哪个Task先结束了,或者抛一个 TimeoutException 或者返回结果。

注意,我很小心地处理了超时情况下的异常。我使用 ContinueWith 来继续原来的 Task,如果存在异常就会进行处理。我知道 Task.Delay 永远也不会抛出异常,因此我无需对它进行处理。HandleException 方法的实现大致如下:

cs 复制代码
private static void HandleException<T>(Task<T> task)
{
    if (task.Exception != null)
    {
        logging.LogException(task.Exception);
    }
}

很明显,此处如何做取决于你对异常的处理策略。通过使用 ContinueWith 关联 HandleException 方法,我确保无论原来的Task何时结束(无论在将来的什么时间完成),检查异常的逻辑都会被执行。重要的是,这不会阻碍程序的主执行流程,因为当超时结束的时候,它已经做了它需要做的事情。

《未完待续》

取消异步操作

异步操作时反馈进度

相关推荐
csdn_aspnet3 小时前
C# 结合 JavaScript 实现手写板签名并上传到服务器
javascript·c#
我不是程序猿儿3 小时前
【C#】软件设计,华为的IPD学习之需求开发心得
学习·华为·c#
WebRuntime3 小时前
所有64位WinForm应用都是Chromium浏览器
javascript·c++·c#·.net·web
superman超哥4 小时前
仓颉Union类型的定义与应用深度解析
开发语言·后端·python·c#·仓颉
唐青枫4 小时前
C#.NET 索引器完全解析:语法、场景与最佳实践
c#·.net
MyBFuture14 小时前
C#接口与抽象类:关键区别详解
开发语言·c#·visual studio
曹牧15 小时前
C#:记录日志
服务器·前端·c#
心疼你的一切17 小时前
三菱FX5U PLC与C#通信开发指南
开发语言·单片机·c#
czhc114007566318 小时前
C# 1221
java·servlet·c#