笔记源于课堂编写:BiliBili

第一课:线程(Thread)创建线程

1什么是线程Thread

  • 线程是一个可执行路径,他可以独立于其他线程执行
  • 每个线程在操作系统的进程(Process)内执行,而操作系统进程提供了程序运行的独立环境。
  • 单线程应用,在进程的独立环境里只跑一个线程,所以该线程拥有独占权。
  • 多线程应用,单个进行中会跑多个线程,他们会共享当前的执行环境(尤其是内存)。
    • 例如:一个线程在后台读取数据,另一个线程在数据到达后进行展示。
    • 这个数据被称作为:共享的状态

001.2-C#异步编程基础 - 图1
**

2.术语:线程被抢占
线程在这个时候就可以成为被强占了:他的执行与另外一个线程上代码的执行交织的那一点。

3.线程的一些属性

  • 线程一旦开始执行,IsAlive就是true,线程结束就变成false
  • 线结束的条件就是:线程的构造函数传入的委托结束了线程
  • 线程一旦结束,就无法再重启。
  • 每个线程都有Name属性,通常用于调试。
    • 线程Name只能设置一次,以后更改会抛出异常。
  • 静态的Thread。CurrentThread属性,会返回当先执行的线程。

4.Join and Sleep

  • 调用Join方法,就可以等待另一个线程结束

5.添加超时

  • 调用Join的时候,可以设置一个超时,用毫秒或者TimeSpan都就可以。
  • Thread.Sleep()方法会暂停当前的线程,并等一段时间。
  • 注意:
    • Thread.Sleep(0)这样调用会导致线程立即放弃本身当前的时间片,自动将CPU移交给其他线程。
    • Thread.YieId做同样的事情,但是他只会把执行交给同一处理器上的其他线程。
    • 当等待Sleep或Join的时候,线程处于阻塞的状态。

第二课:阻塞

1.阻塞

  • 如果线程的执行由于某种原因导致暂停,那么就热为该线程被阻塞了。
    • 例如在Sleep或者通过Join等待其他线程结束
  • 被阻塞的线程会立即将其处理器的时间片生成给其他线程,从此就不在小号处理器时间,直到满足其阻塞条件为止。
  • 可以通过ThreadState这个属性来判断线程是否处于被阻塞的状态。

001.2-C#异步编程基础 - 图2

2.ThreadState枚举

  • ThreadState是一个flags enum,通过按位的形式,可以合并数据的选项。

001.2-C#异步编程基础 - 图3
001.2-C#异步编程基础 - 图4

  • 但是他大部分的枚举值都没什么用,下面的代码将ThreadState剥离为四个最有用的值之一:Unstarted、Running、WaitSleepJoin和Stopped。

001.2-C#异步编程基础 - 图5

2.解除阻塞

  • 阻塞条件被满足
  • 操作超时(如果设置超时的话)
  • 通过Thrread.Interrupt()进行打断
  • 通过Thread.Abort()进行中止

3.上下文切换
当线程阻塞或接触阻塞时,操作系统将执行上下文切换,折回去产生少量的开销,通常为1或2微秒。

3.I/O-bound 和 Compute-bound(或CPU-Bound)

  • 一个花费大部分时间等待某事发生的操作称为I/O-bound
    • I/O绑定通常涉及输入或者输出,但这不是硬性要求:Thread.Sleep()也被称为I/O-bound
  • 相反,一个花费大部分时间执行CPU密集型工作的操作称为Compute-bound

4.阻塞 和 忙等待(自旋)

  • I/O-bound操作的工作方式有两种:
    • 在当前线程上同步的等待
      • Console.ReadLine()、Thread.Sleep()、Thread.Join()
    • 异步的操作,在稍后操作完成时触发一个回调动作。
    • 同步等待的I/O-bound操作将大部分时间花在阻塞线程上。
    • 他们也可以周期性的在一个循环体进行”打转(自旋)”

001.2-C#异步编程基础 - 图6

区别:

  • 首先,如果您希望条件很快的到满足(可能在几微妙之内),则短暂自旋可能会很有效,因为他避免了上下文切换的开销和延迟。
    • .NET Framework提供了特殊的方法和类来提供帮助SpinLock和SpinWait
  • 其次,阻塞也不是零成本。这是因为每个线程在生存期间会占用大约1MB的内存,并会给CLR和操作系统带来持续的管理开销。
    • 因此,在需要处理成百上千个并发操作的大量I/O-bound程序的上下文中,阻塞可能会很麻烦
    • 所以,此程序需要使用基于回调的方法,在等待时完全撤销其线程。

第三课:什么是线程安全

1.本地 和 共享的状态

  • Local本地独立:
    • CLR为每个线程分配自己的内存栈(Stack),以便使本地变量保持独立。
  • Shared共享:(缺乏线程安全)
    • 如果多个线程都引用同一个对象的实例,那么他们就共享了数据。
    • 被Lambda表达式或匿名委托所捕获的本地变量,会被编译器转换为字段(FieId),所以也会被共享。
    • 静态字段(FieId)也会在线程间共享数据。

2.线程安全
001.2-C#异步编程基础 - 图7

3.锁定与线程安全 简介

  • 在读取和写入共享数据的时候,通过使用一个互斥锁(exclusice lock),就可以修复前面例子的问题。
  • C#使用lock语句来加锁
  • 当两个线程同时竞争一个锁的时候(锁可以基于任何引用了类型的对象),一个线程会携带或阻塞,直到变成可用状态。
  • 在多线程上下文中,以这种方式避免不确定性的代码叫做线程安全
  • Lock不是线程安全的银弹,很容易忘记对字段加锁,lock也会引起一些问题(死锁)。

第四课:向线程传递数据 和 异常处理

1.异常处理

  • 创建线程时在作用范围内的try/catch/finally块,在线程开始执行后与线程无关了。
  • 在WPF、WinForm里,可以订阅全局异常处理事件:
    • Application.DispatcherUnHandledException
    • Application.ThreadException
    • 在通过消息循环调用的程序的任何不认发生未处理的异常(者相当于应用程序活动状态时在主线程上运行的所有代码)后,将触发这些异常。
    • 但是非UI线程上的未处理异常,并不会触发他。
  • 而任何线程有任何为处理的异常都会触发
    • AppDomain.CurrentDomain.UnhandledException

第五课:前台(Foreground) 和 后台线程(Background Threader)

1.前台和后台线程

  • 默认情况下,我们手动创建的线程就是前台线程
  • 只要有前台线程在运行,那么应用程序就会一直处理活动状态。
    • 但是后台程序却不行
    • 一旦所有的前台线程停止,那么应用程序就停止了
    • 任何的后台线程也会突然停止。
  • 注意:线程的前台、后台状态与他的优先级无关(所分配的执行时间)
  • 我们可以通过IsBackground属性判断线程是否是后台线程。

第六课:线程优先级

1.线程优先级

  • 线程的优先级(Thread的Priority属性)决定了相对于操作系统中其他活跃线程所占的执行时间。
  • 优先级分为:
    • enum TreadPriority{Loweat,BelowNormal,Mormal,AboveNormal,Highest}

2.提升线程优先级

  • 提升线程优先级的时候需要特别注意,因为他可能”饿死”其他线程。
  • 如果想让某线程(Thread)的优先级比其他进程(Process)中的线程(Thread)高,那就必须提升进程(Process)的优先级。
    • 使用System.Diagnostics下的Process类。

001.2-C#异步编程基础 - 图8

  • 这可以很好的用于制作少量工作需要较低延迟的非UI进程。
  • 对于大量计算的应用程序(尤其是有UI的应用程序,提高进程优先级可能会使其他线程饿死,从而降低整个计算机的速度)。

第七课:信号(Signaling)

  • 有时,你需要让某线程一直处于等待的状态,直至接受到其他线程打来的通知者叫做signaling(发送信号)。
  • 最简单的信号结构就会ManualResetEvent。
    • 调用它上面的WaitOne方法会阻塞当前线程,直到另一个线程通过调用Set方法来开启信号。
    • 调用完Set之后,信号会与”打开”的状态。可以通过调用Reset方法将其再次关闭。

第八课:同步上下文(Synchronization Contexts)

  • 在System.ComponentModel下有一个抽象类:SynchronizationContext,使得Thread Marshaliing得到泛化。
  • 针对移动、桌面(WPF,UWP,WinForms)等富客户端应用的API,他们都定义和实例化了SynchronizationContext的子类。
    • 可以通过静态属性SynchronizationContext.current来获得(当运行在UI线程时)
    • 捕获该属性让你可以在稍后的时候从worker线程向UI线程发送数据。
    • 调用Post就相当于调用Dispatcher或Control上面的BeginInvoke上面
    • 还有一个Send方法,他等价于Invoke方法。

第九课:线程池

1.线程池(Thread Poll)

  • 当开始一个线程的时候,将花费几百微秒来组织类似以下的内容:
    • 一个新的局部变量栈(Stack)
  • 线程池就可以节约这种开销:
    • 通过预先创建一个可循环使用线程的池来减少这一开销。
  • 线程池对于高效的并行编程和细粒度并发是必不可少的。
  • 他允许在不被线程启动的开销淹没的情况下短期操作。

2.使用线程池需要注意以下几点

  • 不可以设置池线程Name
  • 线程池都是后台线程
  • 阻塞池线程可使性能降级
  • 你可以自由的更改池线程的优先级
    • 当他释放回池的时候优先级还原为正常状态
  • 可以通过Thread.CurrentThread.IsThreadPollThread属性来判断是否执行在池线程上。

3.进入线程池

  • 最简单的、显示的在池线程运行代码的方式就是使用Task.Run
  • 例子:Task.Run(()=>Console.WriteLIne(“你好”));

4.谁使用了线程池

  • WFF,Remoting,ASP.NET,ASMX Web Services应用服务器
  • System.Timers.Timer,System.Threading.Timer
  • 并行编程结构
  • BackgroundWorker类(现在很多余)
  • 异常委托(现在很多余)

5.线程池中的整洁

  • 线程池提供了另一个功能,即确保临时超出 计算-Bound的工作不会导致CPU超额订阅
    • CPU超额订阅:活跃的线程超过CPU的核数,操作系统就需要对线程进行时间切片
  • 超额订阅对性能影响很大,时间切片需要昂贵的上下文切换,并且可能使CPU缓存失效,而CPU缓存对于现代处理器的性能至关重要。

6.线程中的整洁-CLP的策略

  • CLR通过对任务排队并其启动进行节流显示来避免线程池中的超额订阅。
  • 他首先运行可能多的并发任务(只要还有CPU核),然后通过爬山算法调整并发级别,并在特定方向上不断调整工作负载。
    • 如果吞吐量提高,他将继续朝同一个方法(否则反转)
  • 这确保他始终追随最佳性能曲线,即使面对计算机上竞争的进程活动时也是如此。
  • 如果下面两个条件能够满足,那么CLR的策略将发挥出最佳效果:
    • 工作项目太多时短时间运行的(<250毫秒,或者理想情况下<100毫秒),因此CLR有很多机会进行测量和调整。
    • 大部分时间都被阻塞的工作项不会主宰线程池。
  • 如果过想充分利用CPU,那么保持线程池的”整洁”是非常重要的。

第十课:Task

1.Thread的问题

  • 线程(Thread)是用来创建并发(concurrentcy)的一种低级别工具,他有一些限制,尤其是:
    • 虽然开始线程的时候可以方便的传入数据,但是当Join的时候,很难从线程获得返回值。
      • 可能需要设置一些共享字段。
      • 如果操作抛出异常,捕获和传播该异常都很麻烦。
    • 无法告诉线程在结束时做另外的工作,你必须进行Join操作(在进程中阻塞当前的线程)
  • 很难使用较小的并发(concurrent)来组建大型的并发。
  • 导致了对手动同步的更大依赖以及随之而来的问题。

2.Task Class

  • Task类很好解决上述问题
    • Task是一个相对高级的 抽象:他代表了一个并发操作(concurrent)
      • 该操作可能有Thread支持,或者不由Thread支持
    • Task是可能组合(可使用Continuation吧他们串成链)
      • Task可以使用线程池来减少启动延迟
      • 使用TaskCompletionSource,Tasks可以利用回调的方式,在等待I/O吧规定操作时完全避免线程。

3.开始一个Task(Task.Run)

  • Task类在System.Threading.Tasks命名空间下
  • 开始一个Task最简单的办法就是使用Task.Run(.NET 4.5,4.0的时候是Task.Factory.StartNew)这个静态方法:
    • 传入一个Action委托即可
  • Task默认使用线程池,也就是后台线程
    • 当主线程结束时,你创建的所有Tasks都会结束。
  • Task.Run之后,我们没有调用Task对象,可以用来监视其过程。
    • 在Task.Run之后,我们没有调用Start,因为该方法创建的是”热”任务(hot task)。
      • 可以通过Task的构造函数创建”冷”任务(cold task),但是很少这样做。

4.Wait等待

  • 调用taskde wait方法会进行阻塞直到操作完成。
    • 相当于调用Thread上的Join方法。

5.长时间运行的任务(Long-running tasks)

  • 默认情况下,CLR在程序池中运行Task,这非常适合短时间运行的Compute-Bound(计算类)类工作。
  • 针对长时间运行的任务或者阻塞操作,可以不采用线程池。
  • 如果同时运行多个lomg-running tasks(尤其是其中有多处于阻塞状态的),那么性能将受到恨到影响,这时有比TaskCreationOptions.LongRunning更好的办法:
    • 如果任务是IO-Bound,TaskCompletionSource和异步函数可以让你用回调(Coninuations)代替线程来实现并发。
    • 如果任务是Compute-Cound,生产者/消费者队列允许你对任务的并发性进行限流,避免把其他线程饿死。

第十一课:Task返回值

1.Task的返回值

  • Task有一个泛型子类叫做Task,他允许发出一个返回值。
  • 使用Func委托或兼用的Lambda表达式来调用Task.Run就可以得到Task
  • 随后,可以通过Result属性来获得返回的结果。
    • 如果这个task还没有完成操作,访问Result属性会阻塞该线程直到该task完成操作。
  • Task可以看做一种所谓的”未来/许诺”(future、promise),在他里面包裹着一个result,在稍后的时候就变得可用。
  • 在CTP版本的时候。Task世纪叫做Future

第十二课:Task异常

1.Task的异常

  • 与Thread不一样,Task可以很方便的传播异常。
    • 如果你的task里面抛出了一个未处理的异常(故障),那么该异常就会重新被抛出给:
      • 调用了wait()的地方
      • 访问了Task属性的地方
  • CLR将异常包裹在AggregateException里,以便在并行编程场景中发挥很好的作用。

2.不抛出异常,怎么检测是否有异常?

  • 无需重新抛出异常,通过Task的IsFaulted和IsCanceled属性也可以检测出Task是否发生了故障。
    • 如果两个属性返回false,说明没有错误发生。
    • 如果IsCanceled为true,那么说明一个OperationCanceledException为该Task抛出了。
    • 如果IsFaulted为true,那就说明另一个类型的异常被抛出了,而Exception属性也将指明错误。

3.异常与”自治”的Task

  • 自治的:”设置完就不管了”的Task。就是指不通过调用Wait()方法,Result属性或continuation进行会合的任务。
  • 针对自治的Task,需要像Thread一样,显示的处理异常,避免发生”悄无声息的故障”。
  • 自治Task上未处理的异常称为:未观察到的异常

4.未观察到的异常

  • 可以通过全局的TaskScheduler,UnobservedTaskException来订阅未观察到的异常。
  • 关于什么是”未观察到的异常”,有一些细微的差别:
    • 使用超时进行等待的Task,如果在超时后发生故障,那么他将会产生一个”未观察到的异常”
    • 在Task发生故障后,如果访问Task的Exception属性,那么该异常就被认为是”以观察到的”。

第十三课:Continuation

1.Continuation

  • 一个Continuation会对Task说:”当你结束的时候,继续在做点其他的事情”
  • Continuation通常通过回调的方式实现的。
    • 当操作一结束,就开始执行
  • 在task上调用GetAwaiter会返回一个awaiter对象
    • 他的OnCompleted会告诉之前的task:”当你结束/发生故障的时候要执行委托”
  • 可以将Continuation附加到已经结束的task上面,此时Continuation将会被安排执行。

2.awaiter

  • 任何可以暴露下列两个方法和一个属性的对象就是awaiter
    • OnComplated
    • GetResult
    • 一个叫做IsComplated的bool属性
  • 没有接口或者父类来统一这些成员
  • 其中OncOompleted是INotityCompletion的一部分

3.如果发生故障

  • 如果之前的任务发生故障,那么当Continuation代码调用awaiter.GetResult()的时候,异常会被重新抛出。
  • 无需调用GetResult,我们可以直接访问task的Result属性
  • 但调用GetResult的好处是,如果task发生故障,那么异常会被直接抛出,而不是包裹在AggregateException里面,这样catch快就简洁很多了。

4.非泛型task

  • 针对非泛型的task,GetResult()方法有一个void返回值,他就是用来重新抛出异常。

5.同步上下文

  • 如果同步上下文出现了,那么OnCompleted会自动捕获他,并将Continuaation提交到这个上下文中,这一点在富客户端应用中非常有用,因为他会把Continuation放回到UI线程中。
  • 如果是编写一个库,则不希望出现上述行为。因为开销较大的UI线程应该在程序运行离开库的时候只发生一次,而不是出现在调用之间。所以,我们可以使用CongigureAwait方法来避免这种行为。
  • 如果没有同步上下文出现,或者你使用的是ConfigureAwait(false),那么Continuation会出现在先前task的同一个线程上,从而避免不需要的开销。

6.ContinueWith

  • 另外一种附加Continuation的方式就是调用task的ContinueWith方法。
  • ContinueWith本身返回一个task,他可以用它来附加更多的Continuation。
  • 但是,必须直接处理AggregateException:
    • 如果task发生故障,需要写额外的代码来吧Continuation封装(marshal)到UI应用上。
    • 在非UI上下文中,若想让Continuation和task执行在同一个线程上,必须指定TaskContinuationOptuibs.ExecuteSynchronsly,否则他将回到线程池。
  • ContinueWith对于并行编程来说非常有用。

第十四课:TaskCompletionSource

1.TaskCompletionSource

  • Task.Run创建Task
  • 另一种方式就是用TaskCompletionSource来创建Task
  • TaskCompletionSource让你在稍后开始和结束的任意操作创建Task
    • 他会为你提供一个手动执行的”从属”Task
      • 指示操作何时结束或发生故障
  • 他对IO-Bound类工作比较理想
    • 可以获得所有Task的好处(传播值、异常、Continuation)、
    • 不需要再操作时阻塞线程

2.使用TaskCompletionSource

  • 初始化一个实例即可
  • 他有一个Task属性可返回一个Task
  • 该Task完全由一TaskCompletionSource对象控制
  • 调用任意一个方法都会给Task发信号:
    • 完成、故障、取消
  • 这些方法只能调用一次,如果再次调用:
    • SetXxx会抛出异常
    • TryXxx会返回false

3.TaskCompletionCource的真正魔力

  • 他创建Task,但并不占用线程

第十四课:同步 和 异步

1.同步与异步

  • 同步操作会在返回调用者之前完成它的工作
  • 异步操作会在返回调用者之后去做的(大部分)工作
    • 异步的方法更为少见,会启用并发,因为他的方法会与调用者并行执行。
    • 异步方法通常很快(立即)回返回到调用者,所以叫非阻塞方法。
  • 目前见到的大部分的异步方法都是通用目的的:
    • Thread.Start
    • Task.Run
    • 可以将continuation附加到Task的方法

2.什么事异步编程?

  • 异步编程的原则是将长时间运行的函数写成异步的。
  • 传统的做法是将长时间运行的函数写成同步的,然后从新的线程或Task进行调用,从而按需引入并发。
  • 上述异步方式的不同之处在于,他是从长时间运行的函数启动并发。这两点好处:
    • IO-Bound并发不可使用线程来实现。可提高可扩展性和执行效率。
    • 富客户端在workerx线程会使用更少的代码,简化了线程的安全性。

3.异步编程的两种用途

  • 编写高效处理大量并发IO的应用程序(典型的:服务端应用)
  • 挑战并不是线程安全(因为共享状态通常是最小化的),而是执行效率
    • 特别的,web每个网络请求并不会消耗一个线程
  • 调用图(call graph):
  • 在富客户端应用里简化线程安全
    • 如果调用图在任何一个操作是长时间运行的,那么整个call graph 必须运行在worker线程上,以保证UI相应。
      • 得到一个横跨读个方法考虑线程安全。
      • 需要为call grapth中的每个方法考虑线程安全。
    • 异步的call gpaph,直到需要才开启一个线程,通常较浅(IO-Bound操作完全不需要)
      • 其他的方法可以在UI线程执行,线程安全简化。
      • 并发的力度适中:
        • 一连串的并发操作,操作之间会弹回到UI线程。

4.经验之谈
001.2-C#异步编程基础 - 图9

第十五课:await

1.异步函数

  • async 和 await 关键字可以让你写出和同步代码一样简洁且结构相同的异步代码

2.awaiting

  • await关键字 简化了附加continuation的过程。

001.2-C#异步编程基础 - 图10

3.async修饰符

  • async修饰符会编译器吧await当做关键字而不是修饰符(c#5以前可能会使用await作为标识符)
  • async修饰符只能应用于方法(包括lambda表达式)。
    • 该方法可以返回void,Task,Task
  • async修饰符对方法的签名或public元数据没有影响(和unsafe一样),他只会影响方法NEIBU .
    • 在接口使用async是没有意义的
    • 使用async嘞重载非async方法是合法的(只要方法签名保持一致)
  • 使用了async修饰符的方法就是”异步函数”

4.异步方法如何执行

  • 遇到await表达式,执行(正常情况下)会返回调用者
    • 就像iterator里面的yield return
    • 在调用前,运行会附加一个continuation到await的task
      • 为保证task结束时,执行会跳出原方法,从停止的地方继续执行。
    • 如果发生发生故障,那么异常会被重新抛出
    • 如果一切正常,那么他的返回值会赋给await表达式

5.可以awati什么?

  • 你await的表达式通常是一个Task
  • 也可以满足一下条件:
    • 有GetAwaiter方法,会返回一个awaiter(实现了INotityCompletion.OnCompleted接口)
    • 返回适当类型的GetResult方法

一个bool类型的IsCompleted属性。

6.捕获本地状态

  • await表达式最牛的地方几乎可以出现任何地方
  • 特别的,在异步方法内,await表达式可以替换任何地方。
    • 出来lock表达式和unsafe上下文。

7.UI上的await
001.2-C#异步编程基础 - 图11
001.2-C#异步编程基础 - 图12

8.与粗粒度的并发相比
001.2-C#异步编程基础 - 图13

第十六课:编写异步函数

  • 对于任何异步函数,你可以使用Task代替void作为返回类型,让该方法成为更有效的异步方法(可以及逆行await。
  • 并不需要再方法体重显示的返回Task。编译器生成一个Task(当方法完成或发生异常时)。这使得创建异步的调用链非常方便。
  • 编译器会对返回Task的异步函数进行扩展,使其成为发送信号或发生孤战时使用TaskCompletionSource来创建Task的代码。

1.编写异步函数富客户端场景下?

  • 富客户端场景下,执行在此刻会调回UI线程(如果目前不在UI线程的话)。
  • 否则,就在Continuation返回任意线程上继续执行。
  • 这意味着,在异步调用图中向上冒泡的时候,不会发生延迟成本,除非是UI线程的第一次”反弹”。

2.返回Task

  • 如果方法体返回TResult,那么异步方法就可以返回Task.
  • 其原理就是给TaskCompetiionSource发送的信号带有值,而不是null。
  • 与同步编程很相似,是故意这样设计的。

3.C#中如何设计异步函数

  • 以同步的方式编写方法
  • 使用异步调用来代替同步调用,并且进行await
  • 除了顶层方法外(UI控件的event handler),吧你方法的返回类型升级为Task或Task,这样他们就可以进行await了。

4.编译器能对异步函数生成Task意味着什么?

  • 大多数情况下,你只需需要在初始化I0-bound并发的底层方法显示的初始化TaskCompletionSource,这种情况是很少见。
  • 针对初始化Compute-bound的并发方法,你可以使用Task.Run来创建Task。

5.异步调用图执行?

  • 整个执行与之前同步例子中调用执行图中的顺序是一样的,因为我们对异步调用函数的调用都进行了await。
  • 在调用图重创建了一个没有并行和重叠的连续流。
  • 每个await在执行中都创建了一个间隙,在间隙后,程序可以中断处恢复执行。

6.异步Lambda表达式?

  • 匿名方法(包括Lambda表达式),通过使用async也可以变成异步方法。
  • 调用方式也是一样的。
  • 附加event handler的时候也可以使用异步的lambda表达式。

第十七课:优化:同步完成

1.优化-同步完成

  • 异步函数可以在await之前就返回
  • 如果URL在缓存中存在,那么不会有await发生,执行就会返回给调用者,方法会返回一个已经设置信号的Task,这就是同步完成。
  • 当await同步完成的Task时,执行不会返回到调用者,也不同通过continuaation调回。他会立即执行到下个语句。

001.2-C#异步编程基础 - 图14
001.2-C#异步编程基础 - 图15
001.2-C#异步编程基础 - 图16
2.注意

  • 对一个同步返回的异步方法进行await,仍然会引起一个小的开销(20纳秒左右,2019年的pc)
  • 返过来,调回到线程池,会引入上下文切换开销,可能是1-2毫秒
  • 而返回到UI的消息循环,至少是10倍开销(如果UI繁忙,那时间更长)

001.2-C#异步编程基础 - 图17

第十八课:ValueTask

001.2-C#异步编程基础 - 图18
001.2-C#异步编程基础 - 图19
001.2-C#异步编程基础 - 图20001.2-C#异步编程基础 - 图21



第十九课:取消

001.2-C#异步编程基础 - 图22
001.2-C#异步编程基础 - 图23001.2-C#异步编程基础 - 图24001.2-C#异步编程基础 - 图25

第二十课:进度报告

001.2-C#异步编程基础 - 图26
001.2-C#异步编程基础 - 图27
001.2-C#异步编程基础 - 图28
001.2-C#异步编程基础 - 图29

第二十一课:TAB

001.2-C#异步编程基础 - 图30

第二十二课:Task组合器

001.2-C#异步编程基础 - 图31
001.2-C#异步编程基础 - 图32
001.2-C#异步编程基础 - 图33
001.2-C#异步编程基础 - 图34
001.2-C#异步编程基础 - 图35
001.2-C#异步编程基础 - 图36
001.2-C#异步编程基础 - 图37

上一篇

001.1-C#语言入门详解

下一篇

001.3-CLR核心机制/垃圾回收/标准Dispose模式