一 异步延迟

在异步方法中,如果需要让程序延迟等待一会后,继续往下执行,应使用Task.Delay()方法。

//创建一个在指定的毫秒数后完成的任务。
public static Task Delay(int millisecondsDelay);
//创建一个在指定的毫秒数后完成的可取消任务。
public static Task Delay(TimeSpan delay, CancellationToken cancellationToken);

下例演示Delay的简单用法:

public static void Main()
{
var t = Task.Run(async delegate
{
await Task.Delay(1000);
return 42;
});
t.Wait();
Console.WriteLine("Task t Status: {0}, Result: {1}",
t.Status, t.Result);
}

下面的例子启动了一个Task,该Task包含对Delay(Int32, CancellationToken)方法的调用,延迟时间为一秒。

token将在延迟时间间隔到期前被取消。因此将引发一个TaskCanceledException,并且Task.Status的属性被设置为Canceled。

public static void Main()
{
CancellationTokenSource source = new CancellationTokenSource(); var t = Task.Run(async delegate
{
await Task.Delay(1000, source.Token);
return 42;
});
source.Cancel();
try
{
t.Wait();
}
catch (AggregateException ae)
{
foreach (var e in ae.InnerExceptions)
Console.WriteLine("{0}: {1}", e.GetType().Name, e.Message);
}
Console.Write("Task t Status: {0}", t.Status);
if (t.Status == TaskStatus.RanToCompletion)
Console.Write(", Result: {0}", t.Result);
source.Dispose();
}

下面的例子用Task.Delay实现了一个简单的超时功能:

static async Task<string> DownloadStringWithTimeout(string uri)
{
using (var client = new HttpClient())
{
var downloadTask = client.GetStringAsync(uri);
var timeoutTask = Task.Delay(3000);
//如果服务在3秒内没有响应,就返回null
var completedTask = await Task.WhenAny(downloadTask, timeoutTask);
if (completedTask == timeoutTask)
return null;
return await downloadTask;
}
}

二 返回完成的Task

2.1 Task.FromResult

如果从异步接口或基类继承代码,但希望用同步的方法来实现它,就会出现这样一个问题,如何实现一个具有异步签名的同步方法?

可以使用 Task.FromResult 方法创建并返回一个新的 Task<T> 对象,这个 Task 对象是已经成功完成的,并有指定的结果。

public static Task<TResult> FromResult<TResult>(TResult result);

如下所示:

interface IMyAsyncInterface
{
Task<int> GetValueAsync();
}
class MySynchronousImplementation : IMyAsyncInterface
{
public Task<int> GetValueAsync()
{
return Task.FromResult(13);
}
}

在用同步代码实现异步接口时,要避免使用任何形式的阻塞操作。在异步方法中进行阻塞操作,然后返回一个完成的 Task 对象,这种做法并不可取。

2.1 TaskCompletionSource

Task.FromResult 只能提供结果正确的同步 Task 对象。如果要让返回的 Task 对象有一个其他类型的结果(例如以 NotImplementedException 结束的 Task 对象),

需要使用TaskCompletionSource :

static Task<T> NotImplementedAsync<T>()
{
var tcs = new TaskCompletionSource<T>();
tcs.SetException(new NotImplementedException());
return tcs.Task;
}

三 报告进度

异步操作执行的过程中,如果需要展示操作的进度,可以考虑使用IProgress<T> 和 Progress<T>。

static async Task CallMyMethodAsync()
{
var progress = new Progress<double>();
progress.ProgressChanged += (sender, args) =>
{
Console.WriteLine($"当前进度:{args}%");
};
await MyMethodAsync(progress);
} static async Task MyMethodAsync(IProgress<double> progress = null)
{
double percentComplete = 0;
while (percentComplete < 100)
{
await Task.Delay(100);
percentComplete++;
if (progress != null)
progress.Report(percentComplete);
}
}

按照惯例,如果不需要报告进度,IProgress<T> 参数可以是 null,因此在 async 方法中一定要对此进行检查。

需要注意的是,IProgress<T>.Report 方法可以是异步的。这意味着真正报告进度之前,MyMethodAsync 方法会继续运行。

基于这个原因,最好把 T 定义为一个不可变类型,或者至少是值类型。如果 T 是一个可变的引用类型,就必须在每次调用 IProgress<T>.Report 时,创建一个单独的副本。

Progress<T> 会在创建时捕获当前上下文,并且在这个上下文中调用回调函数。这意味着,如果在 UI 线程中创建了 Progress<T>,就能在 Progress<T> 的回调函数中更新 UI,

即使异步方法是在后台线程中调用 Report 的。

四 等待一组Task完成

如果需要执行几个Task,等待他们全部完成,可以使用Task.WhenAll方法。

//创建一个任务,该任务将在数组中的所有 Task 对象都已完成时完成。
public static Task WhenAll(params Task[] tasks);

下示简单例子:

Task task1 = Task.Delay(TimeSpan.FromSeconds(1));
Task task2 = Task.Delay(TimeSpan.FromSeconds(2));
Task task3 = Task.Delay(TimeSpan.FromSeconds(1));
await Task.WhenAll(task1, task2, task3);

如果所有任务的结果类型相同,并且全部成功地完成,则 Task.WhenAll 返回存有每个任务执行结果的数组:

Task task1 = Task.FromResult(3);
Task task2 = Task.FromResult(5);
Task task3 = Task.FromResult(7);
int[] results = await Task.WhenAll(task1, task2, task3);
// "results" 含有 { 3, 5, 7 }

Task.WhenAll 方法有以 IEnumerable 类型作为参数的重载,但最好不要使用。只要异步代码与 LINQ 结合,显式的“具体化”序列(即对序列求值,创建集合)就会使代码更清晰:

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);
}

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

但是,如果这个 Task 在被await 调用,就只会抛出其中的一个异常。如果要得到每个异常,可以检查 Task.WhenALl返回的 Task 的 Exception 属性:

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
{
//如果要得到每个异常,可以检查 Task.WhenALl返回的 Task 的 Exception 属性:
AggregateException allExceptions = allTasks.Exception;
}
}

五 等待任意一个Task完成

若需要执行若干个任务,只需要对其中任意一个的完成进行响应。如:对一个操作进行多种独立的尝试,只要一个尝试完成,任务就算完成。

可以使用Task.WhenAny方法,该方法的参数是一批任务,当其中任意一个任务完成时就会返回。作为返回结果的 Task 对象,就是那个完成的任务,即表示提供的任务之一已完成的任务。

public static Task<Task> WhenAny(params Task[] tasks);

下示简单例子:

// 返回第一个响应的 URL 的数据长度。
private static async Task<int> FirstRespondingUrlAsync(string urlA, string urlB)
{
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;
return data.Length;
}

注意,返回的任务将在提供的任何任务完成时完成。

返回的任务将始终以 RanToCompletion 状态结束,其 Result 设置为完成的第一个任务。 即使第一个完成的任务以或Faulted状态结束Canceled,也是如此。

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

注意,第一个任务完成后,考虑是否要取消剩下的任务。如果其他任务没有被取消,也没有被继续 await,那它们就处于被遗弃的状态。

被遗弃的任务会继续运行直到完成,它们的结果会被忽略,抛出的任何异常也会被忽略。

六 Task完成时的处理

如果正在 await 一批任务,希望在每个任务完成时对它做一些处理。另外,希望在任务一完成就立即进行处理,而不需要等待其他任务。

举个例子,下面的代码启动了 3 个延时任务,然后对每一个进行 await。

static async Task<int> DelayAndReturnAsync(int val)
{
await Task.Delay(TimeSpan.FromSeconds(val));
return val;
}
// 当前,此方法输出 2 3 1
// 我们希望它输出 1 2 3
static async Task ProcessTasksAsync()
{
// 创建任务队列。
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;
Trace.WriteLine(result);
}
}

虽然列表中的第二个任务是首先完成的,当前这段代码仍按列表的顺序对任务进行 await。

如果我们希望按任务完成的次序进行处理(例如 Trace.WriteLine),不必等待其他任务,可以考虑下面的方案:

static async Task AwaitAndProcessAsync(Task<int> task)
{
var result = await task;
Trace.WriteLine(result);
} // 现在,这个方法输出 1 2 3
static async Task ProcessTasksAsync()
{
// 创建任务队列。
Task<int> taskA = DelayAndReturnAsync(2);
Task<int> taskB = DelayAndReturnAsync(3);
Task<int> taskC = DelayAndReturnAsync(1);
var tasks = new[] { taskA, taskB, taskC };
var processingTasks = (from t in tasks
select AwaitAndProcessAsync(t)).ToArray();
// 等待全部处理过程的完成。
await Task.WhenAll(processingTasks);
}

上面的代码也可以这么写:

static async Task ProcessTasksAsync()
{
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;
Trace.WriteLine(result);
}).ToArray(); await Task.WhenAll(processingTasks);
}

重构后的代码是解决本问题较清晰、可移植性较好的方法。不过它与原始代码有一个细微的区别。重构后的代码并发地执行处理过程,而原始代码是一个接着一个地处理。

七 避免上下文延续

在默认情况下,一个 async 方法在被 await 调用后恢复运行时,会在原来的上下文中运行。

如果是 UI 上下文,并且有大量的 async 方法在 UI 上下文中恢复,就会引起性能上的问题。

为了避免在上下文中恢复运行,可调用 ConfigureAwait 方法,将其参数continueOnCapturedContext 设为 false来解决:

async Task ResumeOnContextAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
// 这个方法在同一个上下文中恢复运行。
}
async Task ResumeWithoutContextAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
// 这个方法在恢复运行时,会丢弃上下文。
}

八 处理 async Task 方法的异常

可以用简单的 try/catch 来捕获异常,和同步代码使用的方法一样:

static async Task ThrowExceptionAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
throw new InvalidOperationException("Test");
} static async Task TestAsync()
{
// 抛出异常并将其存储在 Task 中。
Task task = ThrowExceptionAsync();
try
{
// Task 对象被 await 调用,异常在这里再次被引发。
await task;
}
catch (InvalidOperationException)
{
// 这里,异常被正确地捕获。
}
}

关于 asnyc void 方法抛出的异常处理,没什么好的方法,如果可以的话,方法的返回类型不要用 void,把它改为 Task。

最好不要从 async void 方法抛出异常。如果必须使用 async void 方法,可考虑把所有代码放在 try 块中,直接处理异常。

以上。

C#并发编程-2 异步编程基础-Task的更多相关文章

  1. C#复习笔记(5)--C#5:简化的异步编程(异步编程的基础知识)

    异步编程的基础知识 C#5推出的async和await关键字使异步编程从表面上来说变得简单了许多,我们只需要了解不多的知识就可以编写出有效的异步代码. 在介绍async和await之前,先介绍一些基础 ...

  2. C#并发编程之异步编程2

    C#并发编程之异步编程(二)   写在前面 前面一篇文章介绍了异步编程的基本内容,同时也简要说明了async和await的一些用法.本篇文章将对async和await这两个关键字进行深入探讨,研究其中 ...

  3. 【憩园】C#并发编程之异步编程(一)

    写在前面 C#5.0中,对异步编程进行了一次革命性的重构,引入了async和await这两个关键字,使得开发人员在不需要深刻了解异步编程的底层原理,就可以写出十分优美而又代码量极少的代码.如果使用得当 ...

  4. .NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)

    本文内容 异步编程类型 异步编程模型(APM) 参考资料 首先澄清,异步编程模式(Asynchronous Programming Patterns)与异步编程模型(Asynchronous Prog ...

  5. 关于如何提高Web服务端并发效率的异步编程技术

    最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...

  6. 如何提高Web服务端并发效率的异步编程技术

    作为一名web工程师都希望自己做的web应用能被越来越多的人使用,如果我们所做的web应用随着用户的增多而宕机了,那么越来越多的人就会变得越来越少了,为了让我们的web应用能有更多人使用,我们就得提升 ...

  7. 【憩园】C#并发编程之异步编程(二)

    写在前面 前面一篇文章介绍了异步编程的基本内容,同时也简要说明了async和await的一些用法.本篇文章将对async和await这两个关键字进行深入探讨,研究其中的运行机制,实现编码效率与运行效率 ...

  8. 【憩园】C#并发编程之异步编程(三)

      写在前面 本篇是异步编程系列的第三篇,本来计划第三篇的内容是介绍异步编程中常用的几个方法,但是前两篇写出来后,身边的朋友总是会有其他问题,所以决定再续写一篇,作为异步编程(一)和异步编程(二)的补 ...

  9. Java网络编程中异步编程的理解

    目录 前言 一.异步,同步,阻塞和非阻塞的理解 二.异步编程从用户层面和框架层面不同角度的理解 用户角度的理解 框架角度的理解 三.为什么使用异步 四.理解这些能在实际中的应用 六.困惑 参考文章 前 ...

  10. C#复习笔记(5)--C#5:简化的异步编程(异步编程的深入分析)

    首先,阐明一下标题的这个“深入分析”起得很惭愧,但是又不知道该起什么名字,这个系列也主要是做一些复习的笔记,供自己以后查阅,如果能够帮助到别人,那自然是再好不过了. 然后,我想说的是异步方法的状态机真 ...

随机推荐

  1. 【Kaggle】如何有效避免OOM(out of memory)和漫长的炼丹过程

    本文介绍一些避免transformers的OOM以及训练等流程太漫长的方法,主要参考了kaggle notebook Optimization approaches for Transformers ...

  2. DP问题大合集

    引入 动态规划(Dynamic Programming,DP,动规),是求解决策过程最优化的过程.20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了 ...

  3. Calendar类介绍_获取对象的方式和Calendar类的常用成员方式

    java.util.Calendar是日历类,在Date后出现,替换掉了许多Date方法.该类将所有可能用到的时间信息封装为静态成员变量,方便获取.日历类就是方便获取各个时间属性的. Calendar ...

  4. 获取某个html元素相对于视窗的位置集合

    getBoundingClientRect() getBoundingClientRect()获取元素位置,这个方法没有参数 getBoundingClientRect()用于获得页面中某个元素的左, ...

  5. Vue3 computed && watch(watchEffect)

    1 # Vue3 计算属性与监视 2 # 1.computed函数:与Vue2.x中的computed配置功能一致 3 inport {ref,computed,watch} from 'vue'; ...

  6. 总结-一本通提高篇&算竞进阶记录

    当一个人看见星空,就再无法忍受黑暗 为了点亮渐渐沉寂的星空 不想就这样退役 一定不会鸽の坑 . 一本通提高篇 . 算竞进阶 . CDQ & 整体二分 . 平衡树 . LCT . 字符串 . 随 ...

  7. CAD二次开发---关于JoinEntity出现eNotApplicable的问题

    作者在使用JoinEntity时出现eNotApplicable的问题,查阅了Autodesk论坛的相关帖子,发现大多数人都有遇到这个问题,但没有找到合适的解决方法,可能原因是进行Join时两Curv ...

  8. kafka报错 日志压缩报错直接退出

    Resetting  first dirty ofset to log start  offset 2971862 since the checkpointed offset 12675089 is ...

  9. java数组---特点,边界

    数组的四个基本特点 1.其长度是确定的.数组一旦被创建,它的大小就是不可以改变的. 2.其元素必须是相同类型,不允许出现混合类型. 3.数组中的元素可以是任何数据类型,包括基本类型和引用类型. 4.数 ...

  10. Java 解析Tiff深入研究

    最近在读取客户发过来的tiff文件是,底层竟然报错了,错误:bandOffsets.length is wrong!   没办法,因为错误消息出现在tiff的read中,因此就对 底层序中tiff读取 ...