C#并发编程-2 异步编程基础-Task

打印 上一主题 下一主题

主题 991|帖子 991|积分 2973

一 异步延迟

在异步方法中,如果需要让程序延迟等待一会后,继续往下执行,应使用Task.Delay()方法。
//创建一个在指定的毫秒数后完成的任务。
public static Task Delay(int millisecondsDelay);
//创建一个在指定的毫秒数后完成的可取消任务。
public static Task Delay(TimeSpan delay, CancellationToken cancellationToken);
下例演示Delay的简单用法:
  1. public static void Main()
  2. {
  3.     var t = Task.Run(async delegate
  4.     {
  5.         await Task.Delay(1000);
  6.         return 42;
  7.     });
  8.     t.Wait();
  9.     Console.WriteLine("Task t Status: {0}, Result: {1}",
  10.                       t.Status, t.Result);
  11. }
复制代码
下面的例子启动了一个Task,该Task包含对Delay(Int32, CancellationToken)方法的调用,延迟时间为一秒。
token将在延迟时间间隔到期前被取消。因此将引发一个TaskCanceledException,并且Task.Status的属性被设置为Canceled。
  1. public static void Main()
  2. {
  3.     CancellationTokenSource source = new CancellationTokenSource();
  4.     var t = Task.Run(async delegate
  5.     {
  6.         await Task.Delay(1000, source.Token);
  7.         return 42;
  8.     });
  9.     source.Cancel();
  10.     try
  11.     {
  12.         t.Wait();
  13.     }
  14.     catch (AggregateException ae)
  15.     {
  16.         foreach (var e in ae.InnerExceptions)
  17.             Console.WriteLine("{0}: {1}", e.GetType().Name, e.Message);
  18.     }
  19.     Console.Write("Task t Status: {0}", t.Status);
  20.     if (t.Status == TaskStatus.RanToCompletion)
  21.         Console.Write(", Result: {0}", t.Result);
  22.     source.Dispose();
  23. }
复制代码
下面的例子用Task.Delay实现了一个简单的超时功能:
  1. static async Task<string> DownloadStringWithTimeout(string uri)
  2. {
  3.     using (var client = new HttpClient())
  4.     {
  5.         var downloadTask = client.GetStringAsync(uri);
  6.         var timeoutTask = Task.Delay(3000);
  7.         //如果服务在3秒内没有响应,就返回null
  8.         var completedTask = await Task.WhenAny(downloadTask, timeoutTask);
  9.         if (completedTask == timeoutTask)
  10.             return null;
  11.         return await downloadTask;
  12.     }
  13. }
复制代码
 
二 返回完成的Task

2.1 Task.FromResult

如果从异步接口或基类继承代码,但希望用同步的方法来实现它,就会出现这样一个问题,如何实现一个具有异步签名的同步方法?
可以使用 Task.FromResult 方法创建并返回一个新的 Task 对象,这个 Task 对象是已经成功完成的,并有指定的结果。
  1. public static Task<TResult> FromResult<TResult>(TResult result);
复制代码
如下所示:
  1. interface IMyAsyncInterface
  2. {
  3.     Task<int> GetValueAsync();
  4. }
  5. class MySynchronousImplementation : IMyAsyncInterface
  6. {
  7.     public Task<int> GetValueAsync()
  8.     {
  9.         return Task.FromResult(13);
  10.     }
  11. }
复制代码
在用同步代码实现异步接口时,要避免使用任何形式的阻塞操作。在异步方法中进行阻塞操作,然后返回一个完成的 Task 对象,这种做法并不可取。
2.1 TaskCompletionSource 

Task.FromResult 只能提供结果正确的同步 Task 对象。如果要让返回的 Task 对象有一个其他类型的结果(例如以 NotImplementedException 结束的 Task 对象),
需要使用TaskCompletionSource :
  1. static Task<T> NotImplementedAsync<T>()
  2. {
  3.     var tcs = new TaskCompletionSource<T>();
  4.     tcs.SetException(new NotImplementedException());
  5.     return tcs.Task;
  6. }
复制代码
 
三 报告进度

异步操作执行的过程中,如果需要展示操作的进度,可以考虑使用IProgress 和 Progress。
  1. static async Task CallMyMethodAsync()
  2. {
  3.     var progress = new Progress<double>();
  4.     progress.ProgressChanged += (sender, args) =>
  5.     {
  6.         Console.WriteLine($"当前进度:{args}%");
  7.     };
  8.     await MyMethodAsync(progress);
  9. }
  10. static async Task MyMethodAsync(IProgress<double> progress = null)
  11. {
  12.     double percentComplete = 0;
  13.     while (percentComplete < 100)
  14.     {
  15.         await Task.Delay(100);
  16.         percentComplete++;
  17.         if (progress != null)
  18.             progress.Report(percentComplete);
  19.     }
  20. }
复制代码
按照惯例,如果不需要报告进度,IProgress 参数可以是 null,因此在 async 方法中一定要对此进行检查。
需要注意的是,IProgress.Report 方法可以是异步的。这意味着真正报告进度之前,MyMethodAsync 方法会继续运行。
基于这个原因,最好把 T 定义为一个不可变类型,或者至少是值类型。如果 T 是一个可变的引用类型,就必须在每次调用 IProgress.Report 时,创建一个单独的副本。
Progress 会在创建时捕获当前上下文,并且在这个上下文中调用回调函数。这意味着,如果在 UI 线程中创建了 Progress,就能在 Progress 的回调函数中更新 UI,
即使异步方法是在后台线程中调用 Report 的。
 
四 等待一组Task完成

如果需要执行几个Task,等待他们全部完成,可以使用Task.WhenAll方法。
  1. //创建一个任务,该任务将在数组中的所有 Task 对象都已完成时完成。
  2. public static Task WhenAll(params Task[] tasks);
复制代码
下示简单例子:
  1. Task task1 = Task.Delay(TimeSpan.FromSeconds(1));
  2. Task task2 = Task.Delay(TimeSpan.FromSeconds(2));
  3. Task task3 = Task.Delay(TimeSpan.FromSeconds(1));
  4. await Task.WhenAll(task1, task2, task3);
复制代码
如果所有任务的结果类型相同,并且全部成功地完成,则 Task.WhenAll 返回存有每个任务执行结果的数组:
  1. Task task1 = Task.FromResult(3);
  2. Task task2 = Task.FromResult(5);
  3. Task task3 = Task.FromResult(7);
  4. int[] results = await Task.WhenAll(task1, task2, task3);
  5. // "results" 含有 { 3, 5, 7 }
复制代码
Task.WhenAll 方法有以 IEnumerable 类型作为参数的重载,但最好不要使用。只要异步代码与 LINQ 结合,显式的“具体化”序列(即对序列求值,创建集合)就会使代码更清晰:
  1. static async Task<string> DownloadAllAsync(IEnumerable<string> urls)
  2. {
  3.     var httpClient = new HttpClient();
  4.     // 定义每一个 url 的使用方法。
  5.     var downloads = urls.Select(url => httpClient.GetStringAsync(url));
  6.     // 注意,到这里,序列还没有求值,所以所有任务都还没真正启动。
  7.     // 下面,所有的 URL 下载同步开始。
  8.     Task<string>[] downloadTasks = downloads.ToArray();
  9.     // 到这里,所有的任务已经开始执行了。
  10.     // 用异步方式等待所有下载完成。
  11.     string[] htmlPages = await Task.WhenAll(downloadTasks);
  12.     return string.Concat(htmlPages);
  13. }
复制代码
如果有一个任务抛出异常,则 Task.WhenAll 会出错,并把这个异常放在返回的 Task 中。如果多个任务抛出异常,则这些异常都会放在返回的 Task 中。
但是,如果这个 Task 在被await 调用,就只会抛出其中的一个异常。如果要得到每个异常,可以检查 Task.WhenALl返回的 Task 的 Exception 属性:
  1. static async Task ThrowNotImplementedExceptionAsync()
  2. {
  3.     throw new NotImplementedException();
  4. }
  5. static async Task ThrowInvalidOperationExceptionAsync()
  6. {
  7.     throw new InvalidOperationException();
  8. }
  9. static async Task ObserveOneExceptionAsync()
  10. {
  11.     var task1 = ThrowNotImplementedExceptionAsync();
  12.     var task2 = ThrowInvalidOperationExceptionAsync();
  13.     try
  14.     {
  15.         await Task.WhenAll(task1, task2);
  16.     }
  17.     catch (Exception ex)
  18.     {
  19.         // ex 要么是 NotImplementedException,要么是 InvalidOperationException
  20.     }
  21. }
  22. static async Task ObserveAllExceptionsAsync()
  23. {
  24.     var task1 = ThrowNotImplementedExceptionAsync();
  25.     var task2 = ThrowInvalidOperationExceptionAsync();
  26.     Task allTasks = Task.WhenAll(task1, task2);
  27.     try
  28.     {
  29.         await allTasks;
  30.     }
  31.     catch
  32.     {
  33.         //如果要得到每个异常,可以检查 Task.WhenALl返回的 Task 的 Exception 属性:
  34.         AggregateException allExceptions = allTasks.Exception;
  35.     }
  36. }
复制代码
 
五 等待任意一个Task完成

若需要执行若干个任务,只需要对其中任意一个的完成进行响应。如:对一个操作进行多种独立的尝试,只要一个尝试完成,任务就算完成。
可以使用Task.WhenAny方法,该方法的参数是一批任务,当其中任意一个任务完成时就会返回。作为返回结果的 Task 对象,就是那个完成的任务,即表示提供的任务之一已完成的任务。
  1. public static Task<Task> WhenAny(params Task[] tasks);
复制代码
下示简单例子:
  1. // 返回第一个响应的 URL 的数据长度。
  2. private static async Task<int> FirstRespondingUrlAsync(string urlA, string urlB)
  3. {
  4.     var httpClient = new HttpClient();
  5.     // 并发地开始两个下载任务。
  6.     Task<byte[]> downloadTaskA = httpClient.GetByteArrayAsync(urlA);
  7.     Task<byte[]> downloadTaskB = httpClient.GetByteArrayAsync(urlB);
  8.     // 等待任意一个任务完成。
  9.     Task<byte[]> completedTask =
  10.     await Task.WhenAny(downloadTaskA, downloadTaskB);
  11.     // 返回从 URL 得到的数据的长度。
  12.     byte[] data = await completedTask;
  13.     return data.Length;
  14. }
复制代码
注意,返回的任务将在提供的任何任务完成时完成。
返回的任务将始终以 RanToCompletion 状态结束,其 Result 设置为完成的第一个任务。 即使第一个完成的任务以或Faulted状态结束Canceled,也是如此。
如果这个任务完成时有异常,这个异常也不会传递给Task.WhenAny 返回的 Task 对象。因此,通常需要在 Task 对象完成后继续使用 await。
注意,第一个任务完成后,考虑是否要取消剩下的任务。如果其他任务没有被取消,也没有被继续 await,那它们就处于被遗弃的状态。
被遗弃的任务会继续运行直到完成,它们的结果会被忽略,抛出的任何异常也会被忽略。
 
六 Task完成时的处理

如果正在 await 一批任务,希望在每个任务完成时对它做一些处理。另外,希望在任务一完成就立即进行处理,而不需要等待其他任务。
举个例子,下面的代码启动了 3 个延时任务,然后对每一个进行 await。
  1. static async Task<int> DelayAndReturnAsync(int val)
  2. {
  3.     await Task.Delay(TimeSpan.FromSeconds(val));
  4.     return val;
  5. }
  6. // 当前,此方法输出 2 3 1
  7. // 我们希望它输出   1 2 3
  8. static async Task ProcessTasksAsync()
  9. {
  10.     // 创建任务队列。
  11.     Task<int> taskA = DelayAndReturnAsync(2);
  12.     Task<int> taskB = DelayAndReturnAsync(3);
  13.     Task<int> taskC = DelayAndReturnAsync(1);
  14.     var tasks = new[] { taskA, taskB, taskC };
  15.     // 按顺序 await 每个任务。
  16.     foreach (var task in tasks)
  17.     {
  18.         var result = await task;
  19.         Trace.WriteLine(result);
  20.     }<br>}
复制代码
虽然列表中的第二个任务是首先完成的,当前这段代码仍按列表的顺序对任务进行 await。
如果我们希望按任务完成的次序进行处理(例如 Trace.WriteLine),不必等待其他任务,可以考虑下面的方案:
  1. static async Task AwaitAndProcessAsync(Task<int> task)
  2. {
  3.     var result = await task;
  4.     Trace.WriteLine(result);
  5. }
  6. // 现在,这个方法输出 1 2 3
  7. static async Task ProcessTasksAsync()
  8. {
  9.     // 创建任务队列。
  10.     Task<int> taskA = DelayAndReturnAsync(2);
  11.     Task<int> taskB = DelayAndReturnAsync(3);
  12.     Task<int> taskC = DelayAndReturnAsync(1);
  13.     var tasks = new[] { taskA, taskB, taskC };
  14.     var processingTasks = (from t in tasks
  15.                            select AwaitAndProcessAsync(t)).ToArray();
  16.     // 等待全部处理过程的完成。
  17.     await Task.WhenAll(processingTasks);
  18. }
复制代码
上面的代码也可以这么写:
  1. static async Task ProcessTasksAsync()
  2. {
  3.     Task<int> taskA = DelayAndReturnAsync(2);
  4.     Task<int> taskB = DelayAndReturnAsync(3);
  5.     Task<int> taskC = DelayAndReturnAsync(1);
  6.     var tasks = new[] { taskA, taskB, taskC };
  7.     var processingTasks = tasks.Select(async t =>
  8.     {
  9.         var result = await t;
  10.         Trace.WriteLine(result);
  11.     }).ToArray();
  12.    
  13.     await Task.WhenAll(processingTasks);
  14. }
复制代码
重构后的代码是解决本问题较清晰、可移植性较好的方法。不过它与原始代码有一个细微的区别。重构后的代码并发地执行处理过程,而原始代码是一个接着一个地处理。
 
七 避免上下文延续

在默认情况下,一个 async 方法在被 await 调用后恢复运行时,会在原来的上下文中运行。
如果是 UI 上下文,并且有大量的 async 方法在 UI 上下文中恢复,就会引起性能上的问题。
为了避免在上下文中恢复运行,可调用 ConfigureAwait 方法,将其参数continueOnCapturedContext 设为 false来解决:
  1. async Task ResumeOnContextAsync()
  2. {
  3.     await Task.Delay(TimeSpan.FromSeconds(1));
  4.     // 这个方法在同一个上下文中恢复运行。
  5. }
  6. async Task ResumeWithoutContextAsync()
  7. {
  8.     await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
  9.     // 这个方法在恢复运行时,会丢弃上下文。
  10. }
复制代码
 
八 处理 async Task 方法的异常

可以用简单的 try/catch 来捕获异常,和同步代码使用的方法一样:
  1. static async Task ThrowExceptionAsync()
  2. {
  3.     await Task.Delay(TimeSpan.FromSeconds(1));
  4.     throw new InvalidOperationException("Test");
  5. }
  6. static async Task TestAsync()
  7. {
  8.     // 抛出异常并将其存储在 Task 中。
  9.     Task task = ThrowExceptionAsync();
  10.     try
  11.     {
  12.         // Task 对象被 await 调用,异常在这里再次被引发。
  13.         await task;
  14.     }
  15.     catch (InvalidOperationException)
  16.     {
  17.         // 这里,异常被正确地捕获。
  18.     }
  19. }
复制代码
关于 asnyc void 方法抛出的异常处理,没什么好的方法,如果可以的话,方法的返回类型不要用 void,把它改为 Task。
最好不要从 async void 方法抛出异常。如果必须使用 async void 方法,可考虑把所有代码放在 try 块中,直接处理异常。
 
以上。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

乌市泽哥

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表