.net 异步编程 全
Thread的问题
线程使用来创建并发(concurrency)的一种低级工具,有一些限制,尤其是:
1.虽然开始线程的时候可以方便地传入数据,但是当Join的时候,很难从线程获得返回值
可能需要设置一些共享字段
如果操作要抛出异常,捕获和传播该异常都很麻烦
2.无法告诉线程在结束时开始做另外的工作,你必须进行Join操作(在进程中阻塞当前线程)
很难使用较小的并发(concurrent)来组建大型的并发
导致了对手动同步的更大依赖以及随之而来的问题
Task class
Task可以解决上述问题
Task是一个相对高级的抽象抽象:他代表了一个并发操作(concurrent)
该操作可能由Thread支持,或不由Thread支持
Task是可组合的(可使用Continuation把他们穿成链)
Tasks可以使用线程池来减少启动延迟
使用TaskCompletionSource,Tasks可以利用回调的方式,在等待I/O绑定操作时完全避免使用线程
开始一个Task
Task.Run
Task类在System.Threading.Task命名空间下
开始一个Task最简单的办法就是使用Task.Run(.NET 4.5,4.0的时候是Task.Factory.StartNew)
-这个静态方法
传入一个Action委托即可
Task.Run(()=> sth)
Task默认使用线程池,也就是后台线程
当主线程结束时,你创建的所有tasks都会结束
Task.Run()返回一个Task对象,可以使用它来监视其过程
在Task.Run之后,我们没有调用Start,因为该方法创建的是“热”任务(hot task)
可以通过Task的构造函数创建“冷”任务(cold task),但很少这样做
可以通过Task.Status属性来跟踪Task的执行状态
调用task的Wait()方法会阻塞直到操作完成
相当于Thread上的Join
Wait也可以指定等待时间或结束令牌来提前结束
Long-running tasks
默认情况下,CLR在线程池中运行Task,这非常适合短时间运行的Compute-Bound类工作
针对长时间运行的任务或者阻塞操作(例如前面的例子),你可以不采用线程池
如果同时运行多个long-running tasks(尤其是其中有阻塞状态的),那么性能会受到很大影响,
-这时有比TaskCreationOptions.LongRunning更好的办法:
如果任务是I/OBound,TaskCompletionSource和异步函数可以让你用回调(Coninuations)代替线程
-来实现并发
如果任务是Conpute-Bound,生产者/消费者队列允许你对任务的并发性进行限流,避免把其他线程和进程饿死
Task返回值
Task有一个泛型子类叫做Task,它允许发出一个返回值
使用Func 委托或兼容的Lambda表达式来调用TaskRun就可以得到Task
随后,可以通过Result属性来获得返回的结果
如果这个Task还没有完成操作,访问Result属性会阻塞该线程直到该task完成操作
Task 可以看作是一种所谓的“未来/许诺”(future、promise [JS]),在它里面包裹着一个Result,
-稍后就会变得可用
在CTP版本的时候,Task实际上叫做Future
Task异常
与Thread不一样,Task可以很方便的传播异常
如果你的task里面抛出了一个未处理的异常,那么该异常就会被重新被抛出给:
调用了Wait()的地方
访问了Task的Result属性的地方
CLR将异常包裹在AggregateException里,以便在并行编程场景中发挥很好的作用
无需重新抛出异常,通过Task的IsFaulted和IsCanceled属性也可检验Task是否异常
两属性都返回false,无异常
IsCanceled为true,说明一个OperationCanceledException为该Task抛出了
IsFaulted为True,说明另一个类型的异常被抛出了,而Exception属性也将指明错误
异常与"自治"的Task
自治的,“设置完就不管了”的Task。就是指不通过调用Wait()方法、Result属性或Ctinuation进行
-会合的任务
针对自治Task,需要像Thread一样,显示的处理异常,避免发生“悄无声息的”故障
自治Task上未处理的异常称为未观察到的异常
可通过全局的TaskScheduler.nobservedTaskException来订阅微观察到的异常
使用超时等待的Task,如果在超时后发生故障,那么他将会产生一个“未观察到的异常”
在Task发生故障后,如果访问Task的Exception属性,那么该异常就被认为是“已观察到的”
Continuation
Task结束后再执行一些事(可以用Task的参数)
Continuation通常用回调的方式执行
操作一结束,就开始执行
在task上调用GetAwaiter会返回一个awaiter对象
它的OnCompleted方法会告诉之前的task:“当你结束/发生故障的时候执行委托”
可以将Continuation附加到已经结束的task上面,此时Continuation将会被安排立即执行
任何可以暴露OnCompleted()、GetResult()和.IsCompleted的bool属性 的对象就是awaiter
没有接口或父类来统一这些成员,其中OnCompleted是INotifyCompletion的一部分
异常
如果之前的任务发生异常,那么Continuation代码调用awaiter.GetResult()的时候,异常会被重新抛出
无需调用GetResult(),我们可以直接访问task的Result属性
但GetResult()的好处是,如果task故障,异常会被直接抛出,而不是包在AggregateException里面,
-这样的话catch块会简洁很多
针对非泛型的task,GetResult()方法有一个void返回值,他就是用来重新抛出异常
同步上下文
如果同步上下文出现了,那么OnCompleted会自动捕获它,并将Continuation提交到这个上下文中。这一点
-在富客户端应用中很有用,因为他会把Continuation放回到UI线程中
如果是编写一个库,则不希望出现上述行为,因为开销较大的UI线程切换应该在程序运行离开库的时候只
-发生一次,而不是出现在方法调用之间。所以,我们可以使用ConfigureAwait()方法来避免这种问题
如果没有同步上下文出现,或者使用的是ConfigureAwait(false),那么Continuation会运行在先前task的
-同一个线程上,从而避免不必要的开销
ContinuaWith()
另外一种附加Continuation的方式就是调用task的ContinueWith()方法
ContinueWith()本身返回一个task,可以用来附加更多的Continuation
但是必须直接处理AggregateException
如果task发生故障,需要写额外的代码来把Continuation封装(marshal)到UI应用上
在非UI上下文中,若想让Continuation和task执行在同一个线程上,必须指定
-TaskContinuationOptions.ExecuteSynchronously,否则它将弹回到线程池
ContinueWith()对并行编程很有用
TaskCompletionSource()
除了task.Run(),另外有一种TaskCompletionSource来创建Task
TaskCompletionSource让你在稍后开始和结束的任意操作中创建Task
它会为你提供一个可守东之星的“从属”Task
指示操作何时结束或发生故障
它对IO-Bound类工作比较理想
可以获得所有Task的好处(传播值、异常、Continuation等)
不需要在操作时阻塞线程
用法
初始化一个实例即可
有一个Task属性可以返回一个Task
该Task完全由TaskCompletionSource对象控制
调用任何一个方法都会给Task发信号:完成、故障、取消
这些方法只能调用一次,若再次调用SetXxx会抛出异常
相当于调用Task.Factory.StartNew,并使用TaskCreationOptions.LongRunning选项来创建非线程池的线程
好处
创建Task,但不占用线程
Task.Delay()
相当于异步的Thread.sleep()
同步VS异步
同步操作会在返回调用结果之前完成它的工作
异步操作会在返回调用结果之后完成它的工作
异步的方法更少见,会启用并发,因为他的工作会与调用者并行执行
异步方法通常很快(立即)就会返回调用者,所以叫非阻塞方法
目前见到的大部分异步方法都是通用目的
Thread.Start
Task.Run
可以将Continuation附加到Task的方法
...
异步编程
原则是将长时间运行的函数写成异步的
传统的做法将长时间运行的函数写成同步的,然后从新的Task进行调用,按需引入并发
上述异步的不同之处在于,他是从长时间运行的函数内部启动并发,有两点好处
I/O-bound并发可不使用线程来实现。可提高可拓展性和执行效率
富客户端在worker线程会使用更少的代码,简化了线程安全性
异步编程的两种用途
编写高效处理大量并发I/O的应用程序(典型的:服务端应用)
挑战并不是线程安全(因为共享状态通常是最小化的),而是执行效率
特别的,每个网络请求并不会消耗一个线程
在富客户端应用里简化线程安全
如果调用图中任何一个操作是长时间运行的,那么整个call graph必须运行在worker线程上
-以保证UI响应
得到一个横跨多个方法的单一并发操作(粗粒度)
需要call graph中的每个方法考虑线程安全
异步的call graph,直到需要才开启一个线程,通常较浅(IO-bound操作完全不需要)
其他的方法可以在UI线程执行,线程安全简化
并发的粒度适中
一传销的并发操作,操作之间会弹回到UI线程
经验之谈
为了获得以上好处,下列操作建议异步编写
IO-bound和Compute-bound操作
执行超过50毫秒的动作
另一方面过细的粒度会损害性能,因为异步操作也有开销
异步编程和Continuation
Task非常适合异步编程,因为他们支持Continuation(他们对异步非常重要)
TaskCompletionSource是实现底层IO-bound异步方法的一种标准方式
对于Compute-bound方法,Task.Run会初始化绑定线程的并发
把task返回调用者,创建异步方法
异步编程的区别:目标是在调用图较低的位置来这样做
富客户端应用中,高级方法可以暴力阻碍UI线程和访问控制以及共享状态上,不会出现线
-程安全问题
语言对异步的支持很重要
需要对task的执行序列化
例如Task B依赖于Task A的执行结果
为此,必须在conmtinuation内部触发下一次循环
async和await
对于不想复杂实现异步非常重要
命令式循环结构不要和Continuation混合在一起,因为它们依赖于当前本地状态
另一种实现,函数式写法(Linq查询),他也是 响应式编程(Rx)的基础
异步函数
async和await关键字可以让你写出和同步代码一样简洁且结构相同的异步代码
await
await关键字简化了附加continuation的过程
其结构如下:
var result = await expression.GetAwaiter();
statements(s);
它的结构相当于:
var aiaiter = expression.GetAwaiter();
aiaiter.OnCompleted(
() => {
var result = aiaiter.GetResult();
statements(s);
}
);
async修饰符
async修饰符会让编译器把await当做关键字而不是标识符(C#5以前可能会使用await作为标识符)
async修饰符只能应用于方法(包括lambda表达式)
该方法可以返回void、Task、Task
async修饰符对方法的签名或public元数据没有影响(和unsafe一样),他只会影响方法内部
在接口内使用async是没有意义的
使用async来重载非async方法却是合法的(只要方法签名一致)
使用了async修饰符的方法就是“异步函数”
可以await什么
你await的表达式通常是一个task
也可以是满足下列条件的任意对象
有GetAwaiter()方法,他返回一个awaiter(实现了INotifyConpletion.OnCompleted接口)
返回适当类型的GetResult方法
一个bool类型的IsCompleted属性
捕获本地状态
await表达式的最牛之处就是他几乎可以出现在任何地方
特别的,在异步方法内,await表达式可以替换任何表达式
除了lock表达式和unsafe表达式
await之后在哪个线程上执行
在执行await表达式之后,编译器依赖于Continuation(通过awaiter模式)来继续执行
如果在富客户端应用的UI线程上,同步上下文会保证后续是在原线程上执行
否则,就会在task结束的线程上继续执行
UI上的await
本例中,只有GetPrimesCountAsync中的代码在worker线程上运行
Go中的代码会“租用”UI线程上的时间
可以说,Go是在消息循环中“伪并发”的执行
也就是说:他和UI线程处理的其他事件是穿插执行的
因为这种伪并发,唯一能发生“抢占”的时刻就是在await期间
这其实简化了线程安全,防止重新进入即可
这种并发发生在调用堆栈较浅的地方(Task.Run调用的代码里)
为了从该模型中获益,真正的并发要避免访问共享状态或UI控件
伪代码:
//为本线程设置同步上下文(WPF)
while(!程序结束){
等着消息队列中发生一些事情
发生了事情,是哪种消息?
键盘/鼠标消息->触发event handler
用户BeginInvoke/消息->执行委托
}
附加到UI元素的event handler通过消息循环执行
因为在UI线程上await,continuation将发送到执行同步上下文上,该同步上下文通过
-消息循环执行,来保证整个Go方法伪装并发的在UI线程上执行
与粗粒度的并发相比
例如使用BackgroundWorker
整个同步调用图都在worker线程上
必须在代码中到处使用Dispatcher.BeginInvoke
循环本身在worker线程上
引入了race condition
若实现取消和过程报告,会使得线程安全问题更容易发生,在方法中新添加任何的代码也是同样的结果
编写异步函数
对于任何异步函数,你可以使用Task代替void作为返回类型,让该方法成为更有效的异步(可await)
并不需要在方法中显示的返回task。编译器会生成一个task(当方法完成或发生异常时),这使得
-创建异步的调用链非常方便
编译器会返回Task的异步函数进行拓展,使其成为当发送信号或发生故障时使用TaskCompletionSource
-来创建Task的代码
因此,当返回Task的异步方法结束的时候,执行就会跳回到对它进行awaot的地方。(通过continuation)
编写异步函数(富客户端场景)
富客户端场景下,执行在此刻会跳回到UI线程(如果目前不在UI线程的话)
否则,就会在continuation返回的任意线程上执行
这意味着,在异步调用图中向上冒泡的时候,不会发生延迟成本,除非是UI线程启动的第一次‘反弹’
返回Task
如果方法返回TResult,那么异步方法就可以返回Task
其原理就是给TaskCompletion发送的信号带有值,而不是null
与同步编程很类似,是故意这么设计的
C#如何设计异步函数
以同步的方法编写程序
使用异步调用来代替同步调用,并且进行await
除了顶层方法外(UI控件的event handler),把你的方法的返回类型升级为Task或Task,
-这样他们就可以进行await了
编辑器能对异步函数生成Task意味着什么
大多数情况下,你只需要在初始化IO-bound并发的底层方法里显式的初始化TaskCompletionSource,
-这种情况很少见
针对初始化compute-bound的并发方法,你可以使用Task.Run来创建Task
异步调用图执行
整个执行与之前同步例子中调用图执行的顺序一样,因为我们对每个异步函数的调用都进行了await
在调用图中创建了一个没有并行和重叠的连续流
每个await在执行中都创建了一个间隙,在间隙后,程序可以从中断处恢复执行
并行
不使用await来调用异步函数会导致并行执行的发生
确实也能满足保持UI相应的并发要求
同样,可以并行跑两个操作
异步Lambda表达式
匿名方法(包括lambda表达式),通过使用async也可以变成异步方法
调用方式也一样
附加event handler的时候也可以使用异步Lambda表达式
myButton.Click += async() => {
await Task.Delay(1000);
myButton.Content = "Done";
}
也可以返回Task
异步中的同步上下文
富客户端应用通常依赖于集中的异常处理事件来处理UI线程上未捕获的异常
例如WPF中的Application.DispatcherUnhandledException
ASP.NET Core中定制ExceptionFilterAttribute也是差不多的效果
其内部原理就是:通过在他们自己的try/catch块来调用UI事件(在ASP.NET Core里就是
-页面处理方法的管道)
发布异常
顶层的异步方法会使事情更加复杂
anync void ButtonClick(object sender,RouteEventArgs args){
await Task.Delay(1000);
throw new Exception("Will this be ignored?");
}
当点击按钮,event handler运行时,在await后,执行会正常的返回到消息循环;1秒钟之后
-抛出的异常无法被消息循环中的catch块捕获
为了缓解该问题,AsyncVoidMethordBuilder会捕获未处理的异常(在返回void的异步方法里),
-并把他们发布到同步上下文(如果出现的话),以确保全局异常处理事件能触发
注意
编译器只会把上述逻辑用于返回类型为void的异步方法
如果ButtonClick的返回类型是Task,那么未处理的异常将导致结果Task出错,然后Task无处可去
发布异常
一个有趣的细微差别:无论你在await前面还是后面抛出异常,都没有区别
因此,下例中,异常会被发布到同步上下文*(如果出现的话),而不会发布给调用者
async void Foo(){ throw null; await Task.Delay(1000);}
如果同步上下文没有出现,异常将会在线程池上传播,从而终止应用程序
发布异常
不直接将异常抛出回调用者的原因是为了确保可预测性和一致性
在下例中,不管someCondition是什么值,InvalidOperationException将始终得到和导致Task出错
-同样的效果
async Task Foo(){
if(someCondition) await Task.Delay(100);
throw new InvalidOperationException();
}
Iterator特是一样的:IEnumerable Foo(){throw null;yield return 123;}
OperationStarted和OperationCompleted
如果存在同步上下文,返回void的异步函数也会在进入函数时调用其OperationStarted方法,
-在函数完成时调用其OperationCompleted方法
如果为了对返回void的异步方法进行单元测试而编写一个自定义的同步上下文,那么
-重写这两个方法确实很有效
优化:同步完成
异步函数可以在await之前就返回
如果URI在缓存中存在,那么不会有await发生,执行就会返回给调用者,方法会返回一个已经
-设置信号的Task,这就是同步完成
当await同步完成的Task时,执行不会返回到调用者,也不同通过continuation跳回。它会
-立即执行到下个语句
编译器是通过检查awaiter上的IsCompleted属性来实现这个优化的。也就是说,无论何时,当你
-await的时候:
Console.WriteLine(await GetWebPageAsync("http://oreilly.com"));
如果是同步完成,编译器会释放可短路continuation的代码
var awaiter = GetWebPageAsync().getAwaiter();
if(awaiter.Iscompleted)
Console.WriteLine(awaiter.GetResult());
else
awaiter.OnCompleted(() => Console.WriteLine(aiaiter.GetResult()));
注意
对一个同步返回的异步方法进行await,仍然会引起一个小的开销(20纳秒左右,2019PC)
反过来,跳回到线程池,会引入上下文切换开销,可能是1-2毫秒
而跳回到UI的消息循环,至少是10倍开销(如果UI繁忙,那时间更长)
优化:同步完成
编写完全没有await的异步方法也是合法的,但是编译器会发出警告
但这类方法可以用于重载virtual/abstract方法
另外一种可以达到相同结果的方式是:使用Task.FromResult,他会返回一个已经设置好信号的Task
Task Foo (){return Task.FromResult("abc");}//效果一样但不会警告
如果从UI线程上调用,那么GetWebPageAsync方法是隐式线程安全的。您可以连续多次调用他
-(从而启动多个并发下载),并且不需要lock来保护缓存
有一种简单的方法可以实现这一点,而不必求助于lock或信令结构。我们创建一个“futures”(Task)
-的缓存,而不是字符串的缓存。注意并没有async:
static Dictionary> _cache = new Dictionary>();
Task GetWebPageAsync(string uri){
if(_cache.TryGetValue(uri,out var downloadTask)) return downloadTask;
return _cache[uri] = new WebClient().DownloadStringTaskAsync(uri);
}
不使用同步上下文,使用lock也可以
lock的不是下载的过程,lock的是检查缓存的过程(很短暂)
lock(_cache){
if(_cache.TryGetValue(uri,out var downloadTask))
return downloadTask;
else
return _cache[uri] = new WebClient().DownloadStringTaskAsync(uri);
}
ValueTask
ValueTask用于微优化场景,您可能永远不需要编写返回此类型的方法
Task和Task是引用类型,实例化它们需要基于堆的内存分配和后续的收集
优化的一种极端形式是编写无需分配此类内存的代码;换句话说,这不会实例化任何引用类型,
-不会给垃圾回收增加负担
为了支持这种模式,C#引入了ValueTask和ValueTask这两个struct,编译器允许使用它们代替
-Task和Task
async ValueTask Foo(){...}
如果操作同步完成,则await ValueTask是无分配的
int answer = await Foo(); //(可能是)无分配的
如果操作不是同步完成的,ValueTask实际上就会创建一个普通的Task(并将await转发给他)
使用AsTask方法,可以把ValueTask转化为Task(也包括非泛型版本)
注意事项
ValueTask并不常见,它的出现纯粹是为了性能
这意味着他被不恰当的值类型语义所困扰,这可能会导致意外。为避免错误行为,必须避免以下情况
多次await同一个ValueTask
操作没结束的时候就调用.GetAwaiter().GetResult()
如果你需要进行这些操作,那么先调用AsTask方法,操作他返回的Task
避免上述陷阱的最简单办法就是直接await方法调用:
await Foo();
将ValueTask赋给变量时,就可能引发错误了
ValueTask valueTask = Foo();
将其立即转化为普通的Task,就可以避免此类错误的发生
Task task = Foo().AsTask();
避免过度的弹回
对于再循环中多次调用的方法,通过调研ConfigureAwait方法,就可以避免重复的弹回到UI消息
-循环所带来的开销
这强迫Task不把continuation弹回给同步上下文。从而将开小学见到接近上下文切换的成本(如果您
-await的方法同步完成,则开销会小很多)
async void A() {... AWAIT b(); ...}
async Task B(){
for(int i=0;i<1000;i++){
await C().Configure.Await(false);
}
}
async Task C(){ ... }
这意味着对于方法B和方法C,我们取消了UI程序中的简单线程安全模型,即代码在UI线程上运行
-并且只能在aiwait语句期间被抢占。但是,方法A不受影响,如果在一个UI线程上启动,他将
-保留在UI线程上
这种优化在编写库时特别重要:您不需要简化线程安全性带来的好处,因为您的代码通常不与
-调用方共享状态,也不访问UI控件
异步模式
取消
cancellation
使用取消标志来实现对并发操作进行取消,可以封装一个类
class CancellationToken{
public bool IsCancellationRequested{get;private set;}
public void Cancel{ IsCancellacionRequested = true;}
pubnlic void ThrowIfCancellationRequested(){
if(IsCancellationRequested)
throw new OperationCanceledException();
}
}
async Task Foo(){
for(int i=0;i<10;i++){
Console.WriteLine(i);
await Task.Delay(1000);
cancellationToken.ThrowIfCancellationRequested();
}
}
当调用者想取消的时候,它调用CancellationToken上的Cancel方法。这就会把
-IsCancellationRequested设置为true,即会导致段时间后Foo会通过
-OprationCanceledException引发错误
CancellationToken和CancellationTokenSource
先不管线程安全(应该在读写IsCancellationRequested时进行lock),这个模式非常有效
-CLR也提供了一个CancellationToken类,它的功能和前面的例子类似
但是他缺少一个cancel方法,Cancel方法在另一个类上进行暴露
CancellationTokenSource
这种分离的设计是出于安全考虑:只能对CancellationToken访问的方法可以检查取消,
-但是不能实例化取消
获取CancellationToken
想获得取消标志(CancellationToken),先实例化CancellationTokenSource:
var cancelSource = new CancellationTokenSource();
这会暴露一个Token属性,他会返回一个CancellationToken,所以我们可以这样调用:
var cancelSource = new CancellationTokenSource();
Task foo = Foo(cancelSource.Token);
...
...(some time later)
cancelSource.Cancel();
delay
CLR里大部分的异步方法都支持CancellationToken,包括Delay方法
...Foo(CancellationToken cancellationToken){
......Task.Delay(1000,cancellationToken); ....
}
这时,task在遇到请求时会立即停止(而不是一秒钟后才停止)
这里,我们无需调用ThrowIfCancellationRequested,因为Delay会替我们做
取消标记在调用栈中很好地向下传播(就像是因为异常,取消请求在调用栈中向上
-级联一样)。
同步方法
同步方法也支持取消(例如Task的Wait方法)。这种情况下,取消指令需要异步发出(
-例如,来自另一个Task)
var cancelSource = new CancellationTokenSource();
Task.Delay(5000).ContinueWith(ant=>cancelSource.Cancel());
其它
事实上,您可以在构造CancellationTokenSource时制定一个时间间隔,以便在一段时间后
-启动取消。它对于实现超时非常有用,无论是同步还是异步:
var cancelSource = new CancellationTokenSource(5000);
try { await Foo (cancelSource.Token);}
catch(OperationCanceledException ex){Console.WriteLine("Cancelled");}
CancellationToken这个struct提供了一个Register方法,他可以让你注册一个回调委托,
-这个委托会在取消时触发。他会返回一个对象,这个对象在取消注册时可以被Dispose掉
编译器的异步函数生成的Task在遇到未处理的OperationCanceledException异常时会自动进入
-取消状态(IsCanceled返回是true,IsFaulted返回是false)
使用Task.Run创教你的Task也是如此。这里是指向构造函数传递(相同的)CancellationToken
在异步场景中,故障Task和取消的Task之间的区别并不重要,因为他们在await时都会抛出
-一个OperationCanceledException。但这在高级并行编程场景(特别是条件continuation)
-中很重要
进度报告
有时,你希望异步操作在运行的过程中能实时的反馈进度。一个简单的解决办法事项异步方法传入
-一个Action委托,当进度变化的时候触发方法调用
Task Foo(Action onProgressPercentChanged){
return Task.Run(()=>{
for(int i =0;i<1000;i++){
if(i%10 == 0) onProgressPercentChanged (i/10);
//sth conpute-bound
}
});
}
Action progess = i => Console.WriteLine(i+" %");
await Foo(progess);
尽管这段代码可以在Console APP中很好的应用,但在富客户端应用中却不理想。因为他是从
-worker线程报告的进度,可能会导致消费者的线程安全问题
IPROGRESS和PROGRESS
CLR提供了一对类型来解决此问题:
IProgress接口
Progress类(实现了上面的接口)
他们的目的就是包装一个委托,以便UI程序可以安全的通过同步上下文来报告进度
接口定义如下
public interface IProgress{
void Report(T value);
}
使用IProgress:
Task Foo (IProgress onProgressPercentChanged){
return Task.Run(() 1=> {
for(intg i = 0;i<1000;i++){
if(i%10 ==0)onProgressPercentChanged.Report(i/10);
//Do sth compute-bound
}
});
}
Progress的一个构造函数可以接受Action类型的委托:
var progress = new Progress (i => Console.WriteLine(i+"%"));
aiait Foo(progress);
Progress还有一个ProgressChanged事件,您可以订阅他,而不是[或附加的]将Action
-委托传递给构造函数
在实例化Progress时,类捕获同步上下文(如果存在)
当Foo调用Report时,委托时通过该上下文调用的
异步方法可以通过将int替换为公开一系列属性的自定义类型来实现更精细的进度报告
TAP(Task-Based Asynchrnous Pattern)
.NET Core 暴露了数百个返回Task且可以await的异步方法(主要和I/O相关)。大多数方法都遵循
-一个模式,叫做基于Task的异步模式(TAP)。这是我们以今为止所描述的合理形式化。
-TAP方法执行以下操作:
返回一个“热”(运行中的)Task或Task
方法名以Async结尾(除了像Task组合器等情况)
会被重载,以便接受CancellationToken或(和)IProgress,如果支持相关操作的话
快速返回调用者(只有很好的初始化同步阶段)
如果是I/O绑定,那么无需绑定线程
Task组合器
异步函数有一个让其保持一致的协议(可以抑制的返回Task),这能让其保持良好的结果:
-可以使用及编写Task组合器,也就是可以组合Task,但是并不关心Task具体做什么的函数
CLR提供了两个Task组合器:
Task.WhenAny
Task.WhenAll
本节课假设定义了以下方法
async Task Delay1(){await Task.Delay(1000); return 1; }
async Task Delay2(){await Task.Delay(2000); return 2; }
async Task Delay3(){await Task.Delay(3000); return 3; }
WhenAny
当一组Task中任何一个Task完成时,Task.WhenAny会返回完成的Task
Task<> winningTask = await Task.WhenAny(Delay1(),Delay2(),Delay3());
Console.WriteLine("Done");
Console.WriteLine(winningTask.Result); //1
因为Task.WhenAny本身就返回一个Task,我们对它进行await,就会返回最先完成的Task
上例完全是非阻塞的,包括最后一行(当访问Result属性时,winningTask已经完成),
-但最好还是对winningTask进行await,因为异常无需AggregateException包装就会重新抛出:
Console.WriteLine(aiait winningTask); //1
实际上,我们可以在一步中执行两个await:
int answer = await await Task.WhenAny(Delay1(),Delay1(),Delay1());
如果“没赢”的Task后续发生了错误,那么异常将不会被观察到,除非你后续对他们进行await(
-或者查询其Exception属性)
WhenAny很适合为不支持超时或取消的操作添加这些功能:
Task task = SomeAsyncFunc();
Task winner = await (Task.WhenAny(task,Task.Delay(5000) ) );
if(winner != task) throw new TimeoutException();
string result = await task; //Unwrap result/re-throw
注意,本例中返回的结果是Task类型
WhenAll
当传给他的所有的Task都完成后,Task.WhenAll会返回一个Task
await Task.WhenAll(Delay1(),Delay2(),Delay3());
本例三秒后结束
通过轮流对3个Task进行await,也可得到类似的结果:
Task task1 = Delay1(),task2 = Delay2,task3 = Delay3;
aiait task1; aiait task2; aiait task3;
不同点是(出了三个await的低效):如果task1出错,我们就无须等待task2和task3了,
-他们的错误也不会被观察到
与之相对,Task.WhenAll知道所有的Task完成,他才会完成,即使有错误发生。如果有错个错误,
-他们的异常会包裹在Task的AggregateException里
await组合的Task,只会跑出第一个异常,想要看到所有的异常,你需要这样做:
Task task1 = Task.Run(() => {throw null;} );
Task task2 = Task.Run(() => {throw null;} );
Task all = task.WhenAll(task1,task2);
try{ aiait all;}
catch{
Console.WriteLine(all.Exception.InnerExceptions.Count); //2
}
对一组Task调用WhenAll会返回Task,也就是所有的Task组合结果
如果进行await,那么就会得到TResult[]:
Task task1 = Task.Run(()=>1);
Task task2 = Task.Run(()=>2);
int[] results = await Task.WhenAll(task1,task2); //{1,2}
实例:
async Task GetTotalSize(string[] uris){
IEnumerable> downloadTasks = uris.Select(uri =>
new WebClient().DownloadDataTaskAsync(uri));
byte[][] contents = await Task.WhenAll(downloadTasks);
return contents.Sum(c => c.Length);
}
async Task GetTotalSize(string[] uris){
IEnumerable> downloadTasks = uris.Select(async uri =>
(await new WebClient().DownloadDataTaskAsync(uri)).Length);
int[] contentLengths = await Task.WhenAll(downloadTasks);
return contentLengths.Sum();
}
自定义Task组合器
可以编写自定义的Task组合器。最简单的组合器接收一个Task,看下例:
async static Task WithTimeout(this Task task,TimeSpan timeout){
Task winner = aiait Task.WhenAny(task,Task.Delay(timeout)).ConfigureAwait(false);
if(winner!= task) throw new TimeoutException();
return aiait task.ConfigureAwait(false); //Unwrap result/re-throw
}
这就是为等待的Task添加了超时的功能
因为这很可能是一个库方法,无需与外界共享状态,所以在await时我们使用了ConfigureAwait(false)
-来避免弹回到UI的同步上下文
通过在Task完成时取消Task.Delay我们可以改进上例的效率(避免了计数器的小开销)
async static Task WithTimeout(this Task task,TimeSpan timeout){
var cancelSource = new CancellationTokenSource();
var delay = Task.Delay(timeout,cancelSource.Token);
Task winner = aiait Task.WhenAny(task,Task.WhenAny(task,delay).ConfigureAwait(false);
if(winner== task)
cancelSource.Cancel();
else
throw new TimeoutException();
return aiait task.ConfigureAwait(false); //Unwrap result/re-throw
}
通过CancellationToken放弃Task
static Task WithCancellation(this Task task,
CancellationTokenToken cancelToken){
var tcs = new TaskCompletionSource();
var reg = cancelToken.Register( () => tcs.TrySetCanceled() );
task.ContinueWith(ant =>{
reg.Dispose();
if(ant.IsCanceled)
tcs.TrySetException(ant.Exception.InnerException);
else
tcs.TrySetResult(ant.Result);
});
return tcs.Task;
}
这个组合器功能类似WhenAll,如果一个Task出错,那么其余的Task也立即出错
async Task WhenAllOrError(params Task[] tasks){
var killJoy = new TaskCompletionSource();
foreach(var task in tasks)
task.ContinueWith(ant => {
if(ant.IsCanceled)
killJoy.TrySetCanceled();
else
killJoy.TrySetException(ant.Exception.InnerException);
});
return await aiait Task.WhenAny(killJoy.Exception,Task.WhenAll(tasks))
.ConfigureAwait(false);
}
这里面TaskCompletionSource的任务就是当任意一个Task出错时,结束工作。所以我们没调用
-SetResult方法,只调用了它的TrySetCanceled和TrySetException方法。在这里ContinuWith
-要比GetAwaiter().OnCompleted更方便,因此我们不访问Task的Result,并且此刻不想弹回到
-UI线程

浙公网安备 33010602011771号