• 《C#并发编程经典实例》学习笔记—2.4 等待一组任务完成


    问题

    执行几个任务,等待它们全部完成。

    使用场景

    • 几个独立任务需要同时进行
    • UI界面加载多个模块,并发请求

    解决方案

    Task.WhenAll 传入若干任务,当所有任务完成时,返回一个完成的任务。

    重载方法

    • Task WhenAll(IEnumerable<Task>)
    • Task WhenAll(params Task[])
    • Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>>)
    • Task<TResult[]> WhenAll<TResult>(params Task<TResult>[])
      举例:
                var task1 = Task.Delay(TimeSpan.FromSeconds(1));
                var task2 = Task.Delay(TimeSpan.FromSeconds(2));
                var task3 = Task.Delay(TimeSpan.FromSeconds(3));
    
                await Task.WhenAll(task1, task2, task3);
    

    当任务返回结果类型相同,所有任务完成返回的是,存着每个任务执行结果的数组。

                var task1 = Task.FromResult(1);
                var task2 = Task.FromResult(2);
                var task3 = Task.FromResult(3);
    
                int[] array = await Task.WhenAll(task1, task2, task3); //{1,2,3}
    

    返回的数组中结果的顺序,并非可控,如上述例子中,只是结果为包含了1,2,3的数组,顺序是不定的。

    书中不建议使用以 IEnumerable 类型作为参数的重载。文中没有介绍作者不建议的原因。我找到作者个人博客的一篇文中中提到如下文字(文章地址:https://blog.stephencleary.com/2015/01/a-tour-of-task-part-7-continuations.html

    The IEnumerable<> overloads allow you to pass in a sequence of tasks, such as a LINQ expression. The sequence is immediately reified (i.e., copied to an array). For example, this allows you to pass the results of a Select expression directly to WhenAll. Personally, I usually prefer to explicitly reify the sequence by calling ToArray() so that it’s obvious that’s what’s happening, but some folks like the ability to pass the sequence directly in.

    该段文字解释了作者更喜欢使用LINQ结合ToArray的方式使用异步,因为作者认为代码会更清晰。书中有例子,如下所示:

            static async Task<string> DownloadAllAsync(IEnumerable<string> urls)
            {
                var httpClient = new HttpClient();
                // 定义每一个 url 的使用方法。
                var downloads = urls.Select(url => httpClient.GetStringAsync(url));
                // 注意,到这里,序列还没有求值,所以所有任务都还没真正启动。
                // 下面,所有的 URL 下载同步开始。
                Task<string>[] downloadTasks = downloads.ToArray();
                // 到这里,所有的任务已经开始执行了。
                // 用异步方式等待所有下载完成。
                string[] htmlPages = await Task.WhenAll(downloadTasks);
                return string.Concat(htmlPages);
            }
    

    如果报错记得添加如下引用

    using System.Linq;
    using System.Net.Http;
    

    返回的Task的状态

                var task1 = ......;
                var task2 = ......;
                var task3 = ......;
    
                Task allTasks = Task.WhenAll(task1, task2, task3);
    

    以上述伪代码为例说明allTasks的状态

    • 当task1出现异常,异常会抛给allTasks,allTasks的状态同task1状态,也是Faulted。
    • 当task1被取消,allTasks的状态是Canceled
    • 当task1, task2, task3,不出现异常,也未被取消,allTasks的状态是RanToCompletion

    Task.WhenAll的异常处理

    上面提到了异常处理,当一个task异常,该异常会被allTasks接收,当多个task异常,这些异常也都会被allTasks接收。但是task1抛异常,task2也出异常,但是try catch 处理await Task.WhenAll(task1, task2);只能抓取其中某一个异常。如何获取所有异常呢?书中列举了两种处理方法,代码如下
    抛出异常的方法

            static async Task ThrowNotImplementedExceptionAsync()
            {
                throw new NotImplementedException();
            }
    
            static async Task ThrowInvalidOperationExceptionAsync()
            {
                throw new InvalidOperationException();
            }
    

    第一种处理方式,只能获取其中一个异常

            static async Task ObserveOneExceptionAsync()
            {
                var task1 = ThrowNotImplementedExceptionAsync();
                var task2 = ThrowInvalidOperationExceptionAsync();
                try
                {
                    await Task.WhenAll(task1, task2);
                }
                catch (Exception ex)
                {
                    // ex 要么是 NotImplementedException,要么是 InvalidOperationException
                    //...
                }
            }
    

    第二种处理方式,可以获取所有异常

            static async Task ObserveAllExceptionsAsync()
            {
                var task1 = ThrowNotImplementedExceptionAsync();
                var task2 = ThrowInvalidOperationExceptionAsync();
                Task allTasks = Task.WhenAll(task1, task2);
                try
                {
                    await allTasks;
                }
                catch
                {
                    AggregateException allExceptions = allTasks.Exception;
                    //...
                }
            }
    

    两种方式的区别是,await调用Task.WhenAll返回的Task对象,即例子中的allTasks,代码 await allTasks;

    作者在书中将对Task.WhenAll的异常处理放在了讨论中,并说明了自己的处理方式

    使用 Task.WhenAll 时,我一般不会检查所有的异常。通常情况下,只处理第一个错误就足够了,没必要处理全部错误。

    显然作者更中意第一种方式。那么你呢?

    参考文章:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task.whenall?view=netcore-2.2

  • 相关阅读:
    Delphi XE2 之 FireMonkey 入门(36) 控件基础: TForm
    Delphi XE2 之 FireMonkey 入门(35) 控件基础: TFmxObject: 其它
    Delphi XE2 之 FireMonkey 入门(39) 控件基础: TScrollBox、TVertScrollBox、TFramedScrollBox、TFramedVertScrollBox
    人月神话之编程行业的乐趣与苦恼
    基于NHibernate的三层结构应用程序开发初步
    .NET设计模式(9):桥接模式(Bridge Pattern)
    Grove,.NET中的又一个ORM实现
    近期学习计划
    .NET设计模式(8):适配器模式(Adapter Pattern)
    [声明]关于春节回家期间不能更新Blog的说明
  • 原文地址:https://www.cnblogs.com/AlienXu/p/10185253.html
Copyright © 2020-2023  润新知