.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线程
posted @ 2022-04-30 17:17  画饼躺平学习冲  阅读(117)  评论(0)    收藏  举报