本篇是异步编程系列的第三篇,原本计划第三篇的内容是介绍异步编程中经常使用的几个方法,可是前两篇写出来后,身边的朋友老是会有其余问题,因此决定再续写一篇,做为异步编程(一)和异步编程(二)的补充。html
本篇内容主要讨论,在咱们的异步代码里,运行的究竟是哪一个线程,在执行长时间运行操做时线程发生了什么。编程
在一个被async修饰了的异步方法里,若是没有遇到await,你的代码将一直在调用线程上。在UI应用程序里,好比ASP.NET或者WinForm程序里,你的代码会在ASP.NET工做线程或WinForm工做线程上运行。安全
咱们来看一下如下范例网络
1: public async Task GetResultAsync()
2: {
3: Console.WriteLine();
4:
5: User user = this.GetUserAsync();
6:
7: //call other code
8:
9: return Task.CompletedTask;
10: }
以上范例里,咱们在一个异步方法里调用了另外一个异步方法,可是咱们并无使用await,这段代码依然在原始调用线程上执行,此时这个方法只是扮演了一个传播异步的做用。框架
当咱们在UI线程上如此编程的时候,代码在UI线程是执行,在没有执行结束以前,页面是没有响应的。因此若是页面长时间没有响应,未必是异步致使的,可能会有其余缘由,须要综合考虑,能够借助性能分析器来查看影响系统的缘由在哪里。异步
代码到达await后,究竟是哪个线程在执行异步操做呢。async
咱们以ASP.NET为例,对于网络请求之类的操做,此时没有线程在执行异步操做,他们都被阻塞了,正在等待操做完成。可是若是使用了Task.Run,那么执行该任务时就要用到线程池里的线程了。异步编程
那么问题来了,咱们在编写异步方法的时候,确确实实能够看到这个方法被执行了,确定有线程执行才行啊。性能
对的,确实须要线程来执行,这个线程咱们把它称之为是IO完成端口线程。此线程等待网络请求完成,同时它在全部网络请求之间共享。当网络请求完成时,操做系统中的中断处理程序会以Job方式添加到IO完成端口的队列中。在请求发起后,响应返回前,它们须要依次由单个IO完成端口处理。优化
实际上,通常状况下只有少许IO完成端口线程,以充分利用多个CPU核心。须要注意的是,不管当前有多少个请求,咱们的线程数量都是固定的。
参考如下运行图
我在异步编程(一)这边文章里,有讲到SynchronizationContext这个类,它是.NET框架提供的类,能够在特定类型的线程中运行代码。
.NET使用各类SynchronizationContext,常见的有ASP.NET、WinForms和WPF使用的UI线程上下文。SynchronizationContext的实例自己并无特殊的地方,其实例指向的是其子类,具备静态成员,能够用于读取和控制当前的SynchronizationContext。
当前SynchronizationContext是当前线程的属性。在一个特定线程所运行到的任意的地方,都可以获取当前的SynchronizationContext并存储它,而且可使用SynchronizationContext,在所启动的这个特定线程上运行代码。综上所述,咱们并不须要知道代码在哪一个线程上启动,只须要使用到SynchronizationContext,咱们就能够返回到启动线程。
SynchronizationContext的重要方法是POST,它可使委托在正确的上下文中运行。
某些SynchronizationContext封装单个线程,如UI线程。有些线程封装了特定类型的线程,例如线程池,但能够选择将委托发送到其中的任何一个线程。有些不会更改代码运行在哪一个线程上,而只用于监视,如ASP.NET SynchronizationContext。
到这个地方,咱们就须要了解一个问题了。在await以前,咱们的代码是在调用线程上运行,那么await以后,恢复方法时到了哪一个线程上了?
实际上,大多数状况下,await后的代码也由调用线程运行,尽管调用线程可能在等待期间作了其余事情。C#使用SynchronizationContext来完成此操做。当等待任务完成时,当前的同步上下文被存储为暂停方法的一部分。而后,当方法恢复时,await关键字的基础结构使用POST在捕获的同步上下文上恢复该方法。
既然有大多数状况,那么确定也有小众状况吧,如下状况能够在不一样的线程上运行
- SynchronizationContext具备多个线程,如线程池
- SynchronizationContext不是真正切换线程的上下文
- 到达等待时,没有当前的同步上下文,例如在控制台应用程序中。
- 将任务配置为不使用同步上下文来恢复
注意:
对于UI应用程序来讲,在同一线程上恢复是最重要的,咱们等待以后安全的操做UI。
以WinForm为例,咱们设计一个按钮,用于下载咱们喜欢的小图标。用户点击按钮以后,UI线程启动,并会执行响应的操做,如下图片展现了一个异步操做的流程,以及期间UI线程与IO线程是如何切换的
一、用户单击该按钮,事件处理程序GetButton_OnClick开始排队等待运行。
二、用户界面线程执行GetButton_OnClick的前半部分,包括对GetFaviconAsync的调用。
三、UI线程继续进入GetFaviconAsync并执行其前半部分,包括对DownloadDataTaskAsync的调用。
四、UI线程继续进入DownloadDataTaskAsync,它启动下载并返回任务。
五、UI线程离开DownloadDataTaskAsync,并返回GgetFaviconAsync处的await。
六、当前的UI线程捕获到了SynchronizationContext。
七、GetFaviconAsyncy由于有await的标识,会等待,当DownloadDataTaskAsync完成后GetFaviconAsyncy便会使用捕获到的SynchronizationContext恢复。
八、用户线程离开GetFaviconAsync,并返回一个任务,并运行到GetButton_OnClick中的await。
九、相似地,GetButton_OnClick被等待暂停。
十、用户线程离开GetButton_OnClick,可能会用于处理其余操做。【此时,咱们正在等待图标下载。可能须要几秒钟。注意,UI线程能够自由处理其余用户操做,而IO完成端口线程还没有涉及到。操做期间阻塞的线程总数为零。】
十一、下载完成,所以IO完成端口在DownloadDataTaskAsync中对逻辑进行排队处理。
十二、IO完成端口线程将把DownloadDataTaskAsync返回的任务设置为完成。
1三、IO完成端口线程在任务内部运行代码并处理完成,并会调用捕获到的同步上下文(UI线程)上的POST以继续运行接下来的代码。
1四、IO完成端口线程被释放并可能在其余IO上工做。
1五、用户界面线程找到POST指令,并继续执行GetFaviconAsync的后半部分,直到结束。
1六、当UI线程离开GetFaviconAsync时,它会将GetFaviconAsync返回的任务设置为完成。
1七、在这个运行点里,当前的同步上下文与捕获的上下文相同,于是无需用到POST,UI线程也会继续同步进行。【此逻辑在WPF中是无效的,由于WPF常常建立新的SynchronizationContext对象。尽管它们是等效的,这使得TPL认为它须要从新POST。】
1八、用户线程继续运行GetButton_OnClick的后半部分,直到结束。
同步上下文的每一个实现都是以不一样的方式执行POST的,这是很是消耗性能的事情。为了不这种开销,.NET内部也是有本身的优化机制的,它会在捕获的SynchronizationContext与任务完成时的当前上下文相同时,不使用POST。颇有意思的是,若是你使用调试器查看这种状况,会发现调用堆栈是颠倒的。
可是,当同步上下文不一样时,这就须要用到系统开销了。在性能关键的代码中或者某个代码库中,若是咱们并不不关心使用到了哪一个线程,这个时候咱们也能够经过本身的手动操做来避开这种开销。
在等待任务以前调用ConfigureaWait来完成。这样就不会恢复到原始同步上下文。
1: byte[] bytes = await httpClient.PostAsJsonAsync(url,data).ConfigureAwait(false).ReadAsStreamAsync();
不过,ConfigureAwait并非严格的指令,它是.NET设计的一个标识,用来告诉运行时咱们不介意方法在哪一个线程上运行。若是该线程不重要(线程池线程),它将会继续执行代码。若是是很重要的线程,.NET会经过自身机制将线程释放,让它来作其余事情,而方法也将在线程池中恢复。.NET使用线程的当前的SynchronizationContext来判断它是否重要。
前文有说过,本文再提一次,在同步代码中运行异步代码,可能有隐藏的问题。Task有一个Result属性,该属性阻止等待任务完成。如如下代码:
1: var result = GetUserAsync().Result;
可是若是在只有一个线程(如UI线程)的SynchronizationContext使用就会发生死锁现象。解决问题的方法就是,咱们可使用线程池线程来解决这个问题。如如下代码:
1: var result = Task.Run(() =>GetUserAsync()).Result;