写在前面

本篇是异步编程系列的第三篇,本来计划第三篇的内容是介绍异步编程中常用的几个方法,但是前两篇写出来后,身边的朋友总是会有其他问题,所以决定再续写一篇,作为异步编程(一)异步编程(二)的补充。

本篇内容主要讨论,在我们的异步代码里,运行的到底是哪个线程,在执行长时间运行操作时线程发生了什么。

Await之前

在一个被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中

代码到达await后,到底是哪一个线程在执行异步操作呢。

我们以ASP.NET为例,对于网络请求之类的操作,此时没有线程在执行异步操作,他们都被阻塞了,正在等待操作完成。但是如果使用了Task.Run,那么执行该任务时就要用到线程池里的线程了。

那么问题来了,我们在编写异步方法的时候,确确实实可以看到这个方法被执行了,肯定有线程执行才行啊。

对的,确实需要线程来执行,这个线程我们把它称之为是IO完成端口线程。此线程等待网络请求完成,同时它在所有网络请求之间共享。当网络请求完成时,操作系统中的中断处理程序会以Job方式添加到IO完成端口的队列中。在请求发起后,响应返回前,它们需要依次由单个IO完成端口处理。

实际上,一般情况下只有少量IO完成端口线程,以充分利用多个CPU核心。需要注意的是,无论当前有多少个请求,我们的线程数量都是固定的。

参考以下运行图

SynchronizationContext

我在异步编程(一)这边文章里,有讲到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线程是如何切换的

1、用户单击该按钮,事件处理程序GetButton_OnClick开始排队等待运行。

2、用户界面线程执行GetButton_OnClick的前半部分,包括对GetFaviconAsync的调用。

3、UI线程继续进入GetFaviconAsync并执行其前半部分,包括对DownloadDataTaskAsync的调用。

4、UI线程继续进入DownloadDataTaskAsync,它启动下载并返回任务。

5、UI线程离开DownloadDataTaskAsync,并返回GgetFaviconAsync处的await。

6、当前的UI线程捕获到了SynchronizationContext。

7、GetFaviconAsyncy因为有await的标识,会等待,当DownloadDataTaskAsync完成后GetFaviconAsyncy便会使用捕获到的SynchronizationContext恢复。

8、用户线程离开GetFaviconAsync,并返回一个任务,并运行到GetButton_OnClick中的await。

9、类似地,GetButton_OnClick被等待暂停。

10、用户线程离开GetButton_OnClick,可能会用于处理其他操作。【此时,我们正在等待图标下载。可能需要几秒钟。注意,UI线程可以自由处理其他用户操作,而IO完成端口线程尚未涉及到。操作期间阻塞的线程总数为零。】

11、下载完成,因此IO完成端口在DownloadDataTaskAsync中对逻辑进行排队处理。

12、IO完成端口线程将把DownloadDataTaskAsync返回的任务设置为完成。

13、IO完成端口线程在任务内部运行代码并处理完成,并会调用捕获到的同步上下文(UI线程)上的POST以继续运行接下来的代码。

14、IO完成端口线程被释放并可能在其他IO上工作。

15、用户界面线程找到POST指令,并继续执行GetFaviconAsync的后半部分,直到结束。

16、当UI线程离开GetFaviconAsync时,它会将GetFaviconAsync返回的任务设置为完成。

17、在这个运行点里,当前的同步上下文与捕获的上下文相同,因而无需用到POST,UI线程也会继续同步进行。【此逻辑在WPF中是无效的,因为WPF经常创建新的SynchronizationContext对象。尽管它们是等效的,这使得TPL认为它需要重新POST。】

18、用户线程继续运行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;

【憩园】C#并发编程之异步编程(三)的更多相关文章

  1. 【憩园】C#并发编程之异步编程(一)

    写在前面 C#5.0中,对异步编程进行了一次革命性的重构,引入了async和await这两个关键字,使得开发人员在不需要深刻了解异步编程的底层原理,就可以写出十分优美而又代码量极少的代码.如果使用得当 ...

  2. C#并发编程之异步编程2

    C#并发编程之异步编程(二)   写在前面 前面一篇文章介绍了异步编程的基本内容,同时也简要说明了async和await的一些用法.本篇文章将对async和await这两个关键字进行深入探讨,研究其中 ...

  3. C#复习笔记(5)--C#5:简化的异步编程(异步编程的基础知识)

    异步编程的基础知识 C#5推出的async和await关键字使异步编程从表面上来说变得简单了许多,我们只需要了解不多的知识就可以编写出有效的异步代码. 在介绍async和await之前,先介绍一些基础 ...

  4. .NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)

    本文内容 异步编程类型 异步编程模型(APM) 参考资料 首先澄清,异步编程模式(Asynchronous Programming Patterns)与异步编程模型(Asynchronous Prog ...

  5. 【憩园】C#并发编程之异步编程(二)

    写在前面 前面一篇文章介绍了异步编程的基本内容,同时也简要说明了async和await的一些用法.本篇文章将对async和await这两个关键字进行深入探讨,研究其中的运行机制,实现编码效率与运行效率 ...

  6. 关于如何提高Web服务端并发效率的异步编程技术

    最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...

  7. 如何提高Web服务端并发效率的异步编程技术

    作为一名web工程师都希望自己做的web应用能被越来越多的人使用,如果我们所做的web应用随着用户的增多而宕机了,那么越来越多的人就会变得越来越少了,为了让我们的web应用能有更多人使用,我们就得提升 ...

  8. Java网络编程中异步编程的理解

    目录 前言 一.异步,同步,阻塞和非阻塞的理解 二.异步编程从用户层面和框架层面不同角度的理解 用户角度的理解 框架角度的理解 三.为什么使用异步 四.理解这些能在实际中的应用 六.困惑 参考文章 前 ...

  9. C#复习笔记(5)--C#5:简化的异步编程(异步编程的深入分析)

    首先,阐明一下标题的这个“深入分析”起得很惭愧,但是又不知道该起什么名字,这个系列也主要是做一些复习的笔记,供自己以后查阅,如果能够帮助到别人,那自然是再好不过了. 然后,我想说的是异步方法的状态机真 ...

随机推荐

  1. 列举Java中常用的包、类和接口

    常用的类: BufferedReader ,BufferedWriter FileReader    ,FileWirter String      ,Integer Date        ,Cla ...

  2. 如何看MySql执行计划explain(或desc)

    简介 MySQL 提供了一个 EXPLAIN 命令, 它可以对 SELECT 语句进行分析, 并输出 SELECT 执行的详细信息, 以供开发人员针对性优化.EXPLAIN 命令用法十分简单, 在 S ...

  3. [Android]自己动手做个拼图游戏

    目标 在做这个游戏之前,我们先定一些小目标列出来,一个一个的解决,这样,一个小游戏就不知不觉的完成啦.我们的目标如下: 游戏全屏,将图片拉伸成屏幕大小,并将其切成若干块. 将拼图块随机打乱,并保证其能 ...

  4. SpringBoot SpEL表达式注入漏洞-分析与复现

    目录 0x00前言 0x01触发原因 0x02调试分析 0x03补丁分析 0x04参考文章 影响版本: 1.1.0-1.1.12 1.2.0-1.2.7 1.3.0 修复方案:升至1.3.1或以上版本 ...

  5. ASP.NET三层架构项目创建流程

    1.进入VS2010,新建项目—>Visual C#—>Web—>ASP.NET空Web应用程序,如图所示: 2.在解决方案处右击—>新建项目—>Windows—> ...

  6. Python猜数小游戏

    使用random变量随机生成一个1到100之间的数 采集用户所输入的数字,如果输入的不符合要求会让用户重新输入. 输入符合要求,游戏开始.如果数字大于随机数,输出数字太大:如果小于随机数,输出数字太小 ...

  7. 入门者必看!SharePoint之CAML总结(实战)

    分享人:广州华软 无名 一. 前言 在SharePoint中,不支持直接操作数据库,但开发过程中,避免不了查询数据,那么,在SharePoint中如何查询数据? 当然是使用CAML语法. 二. 目录 ...

  8. 关于web资金系统提现安全保护,防止极快的重复并发请求导致重复提现的解决思路

    关于WEB金融系统中的提现安全问题很多人没有深入思想,导致有漏洞,常常会遇到有些人遇到被攻击到导资金损失的麻烦,     其实要彻底解决重复并发请求 导致重复提现问题,是需要花点心思的,并没有看起来的 ...

  9. 需求分析&用例编写

    一.需求分析? 1.什么是需求 软件产品必须完成的是以及必须具备的品质. 功能性需求:产品必须完成的那些事,要求一定的功能和品质. 例子:淘宝的用户名登录. 非功能性需求:产品必须具备的属性和品质.诸 ...

  10. python word转pdf

    原理 使用python win32 库 调用word底层vba,将word转成pdf 安装pywin32 pip install pywin32 python代码 from win32com.clie ...