虽然实际 TAP 编程中很少利用到任务的状态,但它是很多 TAP 操作机理的根本,以是下面先从任务状态讲起。
1.任务状态Task 类为异步操作供应了一个生命周期,这个周期由 TaskStatus 列举表示,它有如下值:
publicenum TaskStatus{ Created = 0, WaitingForActivation = 1, WaitingToRun = 2, Running = 3, WaitingForChildrenToComplete = 4, RanToCompletion = 5, Canceled = 6, Faulted = 7}
个中 Canceled、Faulted 和 RanToCompletion 状态一起被认为是任务的终极状态。因此,如果任务处于终极状态,则其 IsCompleted 属性为 true 值。

1.1 手动掌握任务启动
为了支持手动掌握任务启动,并支持布局与调用的分离,Task 类供应了一个 Start 方法。由 Task 布局函数创建的任务被称为冷任务,由于它们的生命周期处于 Created 状态,只有该实例的 Start 方法被调用才会启动。
任务状态平时用的情形不多,一样平常我们在封装一个任务干系的方法时,可能会用到。比如下面这个例子,须要判断某任务知足一定条件才启动:
staticvoidMain(string[] args){ MyTask t = new(() => { // do something. }); StartMyTask(t); Console.ReadKey();}publicstaticvoidStartMyTask(MyTask t){ if (t.Status == TaskStatus.Created && t.Counter>10) { t.Start(); } else { // 这里仿照计数,直到 Counter>10 再实行 Start while (t.Counter <= 10) { // Do something t.Counter++; } t.Start(); }}publicclassMyTask : Task{ publicMyTask(Action action) : base(action) { } publicint Counter { get; set; }}
同样,TaskStatus.Created 状态以外的状态,我们叫它热任务,热任务一定是被调用了 Start 方法激活过的。
1.2 确保任务已激活
把稳,所有从 TAP 方法返回的任务都必须被激活,比如下面这样的代码:
MyTask task = new(() =>{ Console.WriteLine("Do something.");});// 在其它地方调用await task;
在 await 之前,任务没有实行 Task.Start 激活,await 时程序就会一贯等待下去。以是如果一个 TAP 方法内部利用 Task 布局函数来实例化要返回的 Task,那么 TAP 方法必须在返回 Task 工具之前对其调用 Start。
2.任务取消在 TAP 中,取消对付异步方法实现者和消费者来说都是可选的。如果一个操作许可取消,它就会暴露一个异步方法的重载,该方法接管一个取消令牌(CancellationToken 实例)。按照老例,参数被命名为 cancellationToken。例如:
public Task ReadAsync( byte [] buffer, int offset, int count, CancellationToken cancellationToken)
异步操作会监控这个令牌是否有取消要求。如果收到取消要求,它可以选择取消操作,如下面的示例通过 while 来监控令牌的取消要求:
staticvoidMain(string[] args){ CancellationTokenSource source = new(); CancellationToken token = source.Token; var task = DoWork(token); // 实际情形可能是在稍后的其它线程要求取消 Thread.Sleep(100); source.Cancel(); Console.WriteLine($"取消后任务返回的状态:{task.Status}"); Console.ReadKey();}publicstatic Task DoWork(CancellationToken cancellationToken){ while (!cancellationToken.IsCancellationRequested) { // Do something. Thread.Sleep(1000); return Task.CompletedTask; } return Task.FromCanceled(cancellationToken);}
如果取消要求导致事情提前结束,乃至还没有开始就收到要求取消,则 TAP 方法返回一个以 Canceled 状态结束的任务,它的 IsCompleted 属性为 true,且不会抛出非常。当任务在 Canceled 状态下完成时,任何在该任务注册的延续任务仍都会被调用和实行,除非指定了诸如 NotOnCanceled 这样的选项来选择不延续。
但是,如果在异步任务在事情时收到取消要求,异步操作也可以选择不急速结束,而是等当前正在实行的事情完成后再结束,并返回 RanToCompletion 状态的任务;也可以终止当前事情并逼迫结束,根据实际业务情形和是否生产非常结果返回 Canceled 或 Faulted 状态。
对付不能被取消的业务方法,不要供应接管取消令牌的重载,这有助于向调用者表明目标方法是否可以取消。
3.进度报告险些所有异步操作都可以供应进度关照,这些关照常日用于用异步操作的进度信息更新用户界面。
在 TAP 中,进度是通过 IProgress<T> 接口来处理的,该接口作为一个参数通报给异步方法。下面是一个范例的的利用示例:
staticvoidMain(string[] args){ var progress = new Progress<int>(n => { Console.WriteLine($"当提高度:{n}%"); }); var task = DoWork(progress); Console.ReadKey();}publicstaticasync Task DoWork(IProgress<int> progress){ for (int i = 1; i <= 100; i++) { await Task.Delay(100); if (i % 10 == 0) { progress?.Report(i); }; }}
输出如下结果:
当提高度:10%当提高度:20%当提高度:30%当提高度:40%当提高度:50%当提高度:60%当提高度:70%当提高度:80%当提高度:90%当提高度:100%
IProgress<T> 接口支持不同的进度实现,这是由消费代码决定的。例如,消费代码可能只关心最新的进度更新,或者希望缓冲所有更新,或者希望为每个更新调用一个操作,等等。所有这些选项都可以通过利用该接口来实现,并根据特定消费者的需求进行定制。例如,如果本文前面的 ReadAsync 方法能够以当前读取的字节数的形式报告进度,那么进度回调可以是一个 IProgress<long> 接口。
public Task ReadAsync( byte[] buffer, int offset, int count, IProgress<long> progress)
再如 FindFilesAsync 方法返回符合特定搜索模式的所有文件列表,进度回调可以供应事情完成的百分比和当前部分结果集,它可以用一个元组来供应这个信息。
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync( string pattern, IProgress<Tuple<double, ReadOnlyCollection<List<FileInfo>>>> progress)
或利用 API 特有的数据类型:
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync( string pattern, IProgress<FindFilesProgressInfo> progress)
如果 TAP 的实现供应了接管 IProgress<T> 参数的重载,它们必须许可参数为空,在这种情形下,不会报告进度。IProgress<T> 实例可以作为独立的工具,许可调用者决定如何以及在哪里处理这些进度信息。
4.Task.Yield 让步我们先来看一段 Task.Yield() 的代码:
Task.Run(async () =>{ for(int i=0; i<10; i++) { await Task.Yield(); ... }});
这里的 Task.Yield() 实在什么也没干,它返回的是一个空任务。那 await 一个什么也没做的空任务有什么用呢?
我们知道,对打算机来说,任务调度是根据一定的优先策略来安排线程去实行的。如果任务太多,线程不足用,任务就会进入排队状态。而 Yield 的浸染便是让出等待的位置,让后面打消的任务先行。它字面上的意思便是让步,当任务做出让步时,其它任务就可以尽快被分配线程去实行。举个现实生活中的例子,就像你在排队办理业务时,好不容易到你了,但你的事情并不急,志愿让出位置,让其他人先办理,自己假装临时有事到表面溜一圈什么事也没干又回来重新排队。默默地做了一次大善人。
Task.Yield() 方法便是在异步方法中引入一个让步点。当代码实行到让步点时,就会让出掌握权,去线程池表面兜一圈什么事也没干再回来重新排队。
5. 定制异步任务后续操作我们可以对异步任务实行完成的后续操作进行定制。常见的两个方法是 ConfigureAwait 和 ContinueWith。
5.1 ConfigureAwait
我们先来看一段 Windows Form 中的代码:
privatevoidbutton1_Click(object sender, EventArgs e){ var content = CurlAsync().Result; ...}privateasync Task<string> CurlAsync(){ using (var client = new HttpClient()) { returnawait client.GetStringAsync("http://geekgist.com"); }}
想必大家都知道 CurlAsync().Result 这句代码在 Windows Form 程序中会造成去世锁。缘故原由是 UI 主线程实行到这句代码时,就开始等待异步任务的结果,处于壅塞状态。而异步任务实行完后回来准备找 UI 线程连续实行后面的代码时,却创造 UI 线程一贯处于“劳碌”的状态,没空搭理回来的异步任务。这就造成了你等我,我又在等你的尴尬局势。
当然,这种去世锁的情形只会在 Winform 和早期的 ASP.NET WebForm 中才会发生,在 Console 和 Web API 运用中不会生产去世锁。
办理办法很大略,作为异步方法调用者,我们只需改用 await 即可:
privateasyncvoidbutton1_Click(object sender, EventArgs e){ var content = await CurlAsync(); ...}
在异步方法内部,我们也可以调用任务的 ConfigureAwait(false) 方法来办理这个问题。如:
privateasync Task<string> CurlAsync(){ using (var client = new HttpClient()) { returnawait client .GetStringAsync("http://geekgist.com") .ConfigureAwait(false); }}
虽然两种方法都可行,但如果作为异步方法供应者,比如封装一个通用库时,考虑到难免会有新手开拓者会利用 CurlAsync().Result,为了提高通用库的容错性,我们就可能须要利用 ConfigureAwait 来做兼容。
ConfigureAwait(false) 的浸染是见告主线程,我要去远行了,你去做其它事情吧,不用等我。只要先确保一方不在一贯等另一方,就能避免相互等待而造成去世锁的情形。
5.2 ContinueWith
ContinueWith 方法很随意马虎理解,便是字面上的意思。浸染是在异步任务实行完成后,安排后续要实行的事情。示例代码:
privatevoidButton1_Click(object sender, EventArgs e){ var backgroundScheduler = TaskScheduler.Default; var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); Task.Factory .StartNew(_ => DoBackgroundComputation(), backgroundScheduler) .ContinueWith(_ => UpdateUI(), uiScheduler) .ContinueWith(_ => DoAnotherBackgroundComputation(), backgroundScheduler) .ContinueWith(_ => UpdateUIAgain(), uiScheduler);}
如上,可以一贯链式的写下去,任务会按照顺序实行,一个实行完再连续实行下一个。若个中一个任务返回的状态是 Canceled 时,后续的任务也将被取消。这个方法有好些个重载,在实际用到的时候再查看文档即可。
6.总结本文内容都是相比拟较根本的 TAP 异步操作知识点。C# 的 TAP 很强大,供应的 API 大概多,远不止本文讲的这些,都是环绕 Task 转的。关键是要理解好根本操作,才能灵巧利用更高等的功能。希望本文对你有所帮助。
参考:
https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-taphttps://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/implementing-the-task-based-asynchronous-patternhttps://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern