打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
异步编程基础

>>返回《C# 并发编程》

1. 概述

前面的文章介绍了标识了 asyncawait 的代码,是怎么被线程执行的。

>>同步上下文-7.5 异步编程(Async)

下面介绍一些类库和常用的API

2. 报告进度

使用 IProgress<T>Progress<T> 类型

  1. 构造 Progress<T> 实例时捕获当前 同步上下文 实例;

  2. Progress<T> 实例的ProgressChanged 事件被调用时使用上面捕获的同步上下文

  3. 如果在执行构造函数的线程没有同步上下文时(隐含使用的Default同步上下文),则将在 ThreadPool 中调用事件

static async Task DoProgressAsync(int count, IProgress<int> progress = null){    for (int i = 0; i < count; i++)    {        await Task.Delay(200);        if (progress != null)            progress.Report(i + 1);    }}static async Task CallProgressAsync(){    int count = 5;    var progress = new Progress<int>();    progress.ProgressChanged += (sender, args) =>    {        System.Console.WriteLine($"{args}/{count}");    };    await DoProgressAsync(count, progress);}

输出为:

1/52/53/54/55/5

3. 等待一组任务完成

Task.WhenAll 方法有以 IEnumerable 类型作为参数的重载,但建议大家不要使用。

  • 调用 ToListToArray 方法后,序列中没有启动的任务就开始了

static async Task DownloadAllAsync(){    Stopwatch sw = new Stopwatch();    sw.Start();    IEnumerable<string> urls = new string[]{                "https://www.baidu.com/",                "https://cn.bing.com/"            };    var httpClient = new HttpClient();    // 定义每一个 url 的使用方法。    var downloads = urls.Select(url =>    {        Console.WriteLine($"{url}:start");        var res = httpClient.GetStringAsync(url);        res.ContinueWith(t => Console.WriteLine($"{url}:{sw.ElapsedMilliseconds}ms"));        return res;    });    // 注意,到这里,序列还没有求值,所以所有任务都还没真正启动。    // 下面,所有的 URL 下载同步开始。    Task<string>[] downloadTasks = downloads.ToArray();    // 到这里,所有的任务已经开始执行了。    Console.WriteLine($"await Task.WhenAll");    // 用异步方式等待所有下载完成。    string[] htmlPages = await Task.WhenAll(downloadTasks);    Console.WriteLine($"jobs done.");}

输出为:

https://www.baidu.com/:starthttps://cn.bing.com/:startawait Task.WhenAllhttps://www.baidu.com/:270msjobs done.; 由于返回的是请求的 Task 不是 ContinueWith 的打印 Taskhttps://cn.bing.com/:1089ms

4. 异常处理

  • 如果有一个任务抛出异常,则 Task.WhenAll 会出错,并把这个异常放在返回的 Task

  • 如果多个任务抛出异常,则这些异常都会放在返回的 Task

  • 如果这个 Task 在被 await 调用,就只会抛出该异步方法的一个异常

  • 如果要得到每个异常,可以检查 Task.WhenAll 返回的 TaskException 属性:

示例:

static async Task ThrowNotImplementedExceptionAsync(){    await Task.Delay(10);    throw new NotImplementedException();}static async Task<int> ThrowInvalidOperationExceptionAsync(){    TaskCompletionSource<int> completionSource = new TaskCompletionSource<int>();    completionSource.TrySetException(new InvalidOperationException());    return await completionSource.Task;}static async Task ObserveOneExceptionAsync(){    System.Console.WriteLine("OneException");    var task1 = ThrowNotImplementedExceptionAsync();    var task2 = ThrowInvalidOperationExceptionAsync();    try    {        await Task.WhenAll(task1, task2);    }    catch (Exception ex)    {        // ex 要么是 NotImplementedException,要么是 InvalidOperationException        System.Console.WriteLine(ex.GetType().Name);    }}static async Task ObserveAllExceptionsAsync(){    System.Console.WriteLine("AllExceptions");    var task1 = ThrowNotImplementedExceptionAsync();    var task2 = ThrowInvalidOperationExceptionAsync();    Task allTasks = Task.WhenAll(task1, task2);    try    {        await allTasks;    }    catch    {        AggregateException allExceptions = allTasks.Exception;        allExceptions.Handle(ex =>        {            System.Console.WriteLine(ex.GetType().Name);            return true;        });    }}

输出为:

OneExceptionNotImplementedException
AllExceptionsNotImplementedExceptionInvalidOperationException

5. 等待任意一个任务完成

// 返回第一个响应的 URL 的数据长度。private static async Task<int> FirstRespondingUrlAsync(){    string urlA = "https://www.baidu.com/";    string urlB = "https://cn.bing.com/";    var httpClient = new HttpClient();    // 并发地开始两个下载任务。    Task<byte[]> downloadTaskA = httpClient.GetByteArrayAsync(urlA);    Task<byte[]> downloadTaskB = httpClient.GetByteArrayAsync(urlB);    // 等待任意一个任务完成。     Task<byte[]> completedTask = await Task.WhenAny(downloadTaskA, downloadTaskB);    // 返回从 URL 得到的数据的长度。     byte[] data = await completedTask;    Console.WriteLine($"Finish: {(completedTask == downloadTaskA ? nameof(downloadTaskA) : nameof(downloadTaskA))}");    Console.WriteLine($"downloadTaskA: {downloadTaskA.Status}");    Console.WriteLine($"downloadTaskB: {downloadTaskB.Status}");    return data.Length;}

输出为:

Finish: downloadTaskAdownloadTaskA: RanToCompletiondownloadTaskB: WaitingForActivation

如果这个任务完成时有异常,这个异常也不会传递给 Task.WhenAny 返回的 Task 对象。因此,通常需要在 Task 对象完成后继续使用 await

第一个任务完成后,考虑是否要取消剩下的任务。如果其他任务没有被取消,也没有被继续 await,那它们就处于被遗弃的状态。被遗弃的任务会继续运行直到完成,它们的结果会被忽略,抛出的任何异常也会被忽略。

//每个任务需要等到Trace.WriteLine执行完才能执行下一个static async Task<int> DelayAndReturnAsync(int val){    await Task.Delay(TimeSpan.FromSeconds(val));    return val;}// 当前,此方法输出“2”,“3”,“1”。 // 我们希望它先输出先完成的,期望 输出“1”,“2”,“3”。 static async Task ProcessTasksAsync1(){    // 创建任务队列。    Task<int> taskA = DelayAndReturnAsync(2);    Task<int> taskB = DelayAndReturnAsync(3);    Task<int> taskC = DelayAndReturnAsync(1);    var tasks = new[] { taskA, taskB, taskC };    // 按顺序 await 每个任务。     foreach (var task in tasks)    {        var result = await task;        Console.WriteLine(result);    }}//不等Trace.WriteLine切任务并行的解决方案// 现在,这个方法输出“1”,“2”,“3”。 static async Task ProcessTasksAsync2(){    // 创建任务队列。    Task<int> taskA = DelayAndReturnAsync(2);    Task<int> taskB = DelayAndReturnAsync(3);    Task<int> taskC = DelayAndReturnAsync(1);    var tasks = new[] { taskA, taskB, taskC };    var processingTasks = tasks.Select(async t =>    {        var result = await t;        Console.WriteLine(result);    }).ToArray();    // 等待全部处理过程的完成。    await Task.WhenAll(processingTasks);}static async Task ProcessTasksAsyncExe(){    Stopwatch sw = Stopwatch.StartNew();    System.Console.WriteLine("ProcessTasksAsync1");    await ProcessTasksAsync1();    System.Console.WriteLine($"{sw.ElapsedMilliseconds}ms");    sw.Restart();    System.Console.WriteLine();    System.Console.WriteLine("ProcessTasksAsync2");    await ProcessTasksAsync2();    System.Console.WriteLine($"{sw.ElapsedMilliseconds}ms");}

输出为:

ProcessTasksAsync12313007msProcessTasksAsync21233004ms

6. 避免上下文延续

默认情况下,一个 async 方法在被 await 调用后恢复运行时,会在原来的上下文中运行。 如果是 UI上下文 ,并且有大量的 async 方法在 UI上下文 中恢复,就会引起性能上的问题。

  • ConfigureAwait(true) 延续上下文(执行完异步 await ,回到同步上下文)

  • ConfigureAwait(false) 不延续上下文(执行完异步 await ,由于没有记录之前的同步上下文,后续代码在 Default上下文 中运行)

7. async void

处理 async void 方法的异常有一个办法:

  • 一个异常从 async void 方法中传递出来时,会在其同步上下文中引发出来

  • async void方法启动时,同步上下文 就处于激活状态

    • PF 有 Application.DispatcherUnhandledException

    • WinRT 有 Application.UnhandledException

    • ASP.NET 有 Application_Error

    • 如果系统运行环境有特定的 同步上下文(如:UI同步上下文,ASP.Net同步上下文),通常就可以在全局范围内处理这些顶层的异常

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
实用技巧
C# 异步编程
.Net 4.5 异步编程初试(async和await)
Thead,TheadPool,Task,async,await 的前世今生
c# async/await的用法
C#高性能 TCP 服务的多种实现方式
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服