.net core 深挖异步的世界下篇——缺少SynchronizationContext上下文意味着什么?

1,055 阅读9分钟

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

  • 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!
  • 📢本文作者:由webmote 原创,首发于 【掘金】
  • 📢作者格言: 生活在于折腾,当你不折腾生活时,生活就开始折腾你,让我们一起加油!💪💪💪

🎏 序言

最近和.net core中的异步杠上了,我觉得很有必要深挖下.net core下的异步编程的方方面面,由于传统asp.net以及.net framework类库的干扰,因此网上充斥着奇奇怪怪的关于异步的言论和实践,由于没有说明环境的关系,导致大量的.net core console/asp.net core的新手,对于处理异步也存在着深深的误解,因此深挖下其本质区别,对于我自己或更多的.net core新手来说,是有巨大的帮助的。

🎏 5、您不需要ConfigureAwait(false),在编写库中尽量使用它

由于不再有上下文,因此不需要ConfigureAwait(false)。任何知道它在ASP.NET Core下运行的代码都不需要显式地避免其上下文。实际上,ASP.NET Core团队本身已经放弃使用ConfigureAwait(false)。

但是,我仍然建议您在核心库中使用它-可以在其他应用程序中重用的任何东西。如果库中的代码也可以在UI应用程序或旧版ASP.NET应用程序中运行,或者在其他任何可能存在上下文的环境中运行,则仍应ConfigureAwait(false)在该库中使用。

🎏 6、当心隐式并行

从同步上下文移到线程池上下文(即从旧版ASP.NET到ASP.NET Core)时,还有另一个主要问题。

旧式ASP.NETSynchronizationContext是实际的同步上下文,这意味着在请求上下文中,一次只能有一个线程实际执行代码。也就是说,异步延续可以在任何线程上运行,但一次只能运行一个。ASP.NET Core没有SynchronizationContext,因此await默认为线程池上下文。因此,在ASP.NET Core世界中,异步延续可以在任何线程上运行,并且它们都可以并行运行。

作为一个人为的示例,请考虑以下代码,该代码将下载两个字符串并将它们放入列表中。此代码在旧式ASP.NET中可以正常工作,因为请求上下文一次仅允许一个连续:

private HttpClient _client = new HttpClient();

async Task<List<string>> GetBothAsync(string url1, string url2)
{
    var result = new List<string>();
    var task1 = GetOneAsync(result, url1);
    var task2 = GetOneAsync(result, url2);
    await Task.WhenAll(task1, task2);
    return result;
}

async Task GetOneAsync(List<string> result, string url)
{
    var data = await _client.GetStringAsync(url);
    result.Add(data);
}

该result.Add(data)行一次只能由一个线程执行,因为它在请求上下文中执行。

但是,相同的代码在ASP.NET Core上是不安全的。具体来说,该result.Add(data)行可以由两个线程同时执行,而不保护shared List。

这样的代码很少见;异步代码本身具有功能,因此从异步方法返回结果比修改共享状态要自然得多。但是,异步代码的质量确实会有所不同,并且毫无疑问,其中有些代码没有充分地屏蔽并行执行。

7、异步任务需要线程吗?

异步任务不会“执行”,因此不需要线程。这是最纯净的异步基本真理:没有线程。 有巨多的小白反对这一真理。他们喊道:“不,如果我正在等待操作,必须有一个正在等待的线程!这可能是线程池线程。还是操作系统线程!或带有设备驱动程序的东西……”

这里所说的异步任务单纯的指定为IO密集操作,如果是CPU密集操作本质上是同步的。一个线程可以通过将其装载到另一个线程来假装它是异步的。例如,UI线程可以通过将CPU绑定的代码包装在Task.Run中来假装是异步的。从UI线程的角度来看,它似乎是异步的(即,它可以等待操作)。但是从另一个线程(线程池线程)的角度来看,它仍然是同步的,因此看清楚,这里所指的是IO密集型真正的异步。 不要听那些胡言乱语。如果异步操作是纯操作,则没有线程在这里插入图片描述

小白们不相信,让我们来调戏他们。

我们将一直跟踪到硬件的异步操作,尤其要注意.NET部分和设备驱动程序部分。我们将通过省略一些中间层细节来简化此描述,但是我们不会偏离事实。 考虑一个通用的“写”操作(对文件,网络流,USB烤面包机等)。我们的代码很简单:

private async void Button_Click(object sender, RoutedEventArgs e)
{
  byte[] data = ...
  await myDevice.WriteAsync(data, 0, data.Length);
}

我们已经知道UI线程在期间没有被阻塞await。问题:是否有另一个线程必须在“阻止祭坛”上牺牲自己,以便UI线程可以生存?

第一站:库(例如,输入BCL代码)。让我们假设它WriteAsync是使用.NET中基于重叠I / O的标准P / Invoke异步I / O系统实现的。因此,这将在设备的底层启动Win32重叠I / O操作HANDLE。

然后,操作系统转向设备驱动程序,并要求其开始写入操作。为此,首先要构造一个代表写请求的对象。这称为I / O请求包(IRP)。

设备驱动程序接收IRP并向设备发出命令以写出数据。如果设备支持直接内存访问(DMA),则可以像将缓冲区地址写入设备寄存器一样简单。这就是设备驱动程序所能做的;它将IRP标记为“待处理”,然后返回操作系统。

事实的核心在这里:处理IRP时不允许设备驱动程序阻塞。这意味着,如果该IRP无法完成立即,那么它必须被处理异步。即使对于同步API,也是如此!在设备驱动程序级别,所有请求都是异步的

在IRP为“挂起”的情况下,操作系统将返回到库,该库会将未完成的任务返回到按钮单击事件处理程序,该事件处理程序将挂起async方法,并且UI线程将继续执行。

我们已将请求跟踪到系统的深处,一直到物理设备。

写入操作现在处于“运行中”状态。正在处理多少个线程? 答案是没有线程。没有设备驱动程序线程,OS线程,BCL线程或线程池线程正在处理该写操作。没有线程。

现在,让我们关注内核守护程序领域对(用户程序)凡人世界的响应。

写请求开始后的一段时间,设备完成写操作。通过中断通知CPU。

设备驱动程序的中断服务程序(ISR)响应该中断。中断是CPU级别的事件,它使CPU的控制权暂时脱离正在运行的任何线程。您可以将ISR视为“借用”当前正在运行的线程,但我更喜欢将ISR视为执行水平很低,以至于不存在“线程”的概念-因此它们位于所有“之下”线程,可以这么说。

无论如何,ISR均已正确编写,因此它所要做的就是告诉设备“感谢您的中断”,并将延迟过程调用(DPC)排队。

当CPU受中断困扰后,它将进入其DPC。DPC的执行水平也很低,以至于不能说“线程”。与ISR一样,DPC在线程系统“下方”直接在CPU上执行。

DPC接收代表写请求的IRP,并将其标记为“完成”。但是,“完成”状态仅存在于OS级别。进程具有其自己的内存空间,必须通知该内存空间。因此,操作系统会将专用内核模式的异步过程调用(APC)排队到拥有的线程中HANDLE。

由于库/ BCL使用的是标准P / Invoke重叠I / O系统,因此它已向线程池的一部分I / O完成端口(IOCP)注册了句柄。因此,短暂借用了一个I / O线程池线程来执行APC,这将通知任务已完成。

该任务已捕获UI上下文,因此它不会async直接在线程池线程上恢复该方法。而是将方法的延续性排队到UI上下文中,并且UI线程在到达该方法时将继续执行该方法。

因此,我们看到请求进行期间没有线程。请求完成后,各个线程被“借用”或将工作短暂排队。这项工作通常需要大约一毫秒(例如,运行在线程池中的APC)到大约一毫秒(例如,ISR)。但是没有线程被阻塞,只是在等待该请求完成。

释放你的心灵。不要试图找到这个“异步线程”——这是不可能的。相反,请仅尝试了解事实: 没有线程

🎏 8、await后面的代码跑在线程池上

还有一点,await后面的任何代码都将在线程池线程上运行。除非存在SynchronizationContext或,否则这是默认行为TaskScheduler。

这些是最常见的上下文:

  • UI上下文-UI应用程序使用它来确保后面的代码await将在UI线程上恢复。
  • ASP.NET Classic请求上下文-由ASP.NET Classic使用,以确保后面的代码await将恢复具有相同的特定于请求的 全局属性(例如HttpContext.Current)。
  • 单线程上下文-由某些测试框架(例如xUnit)用来确保后面的代码await将在同一线程上恢复。这旨在模拟UI上下文行为,但没有完整的UI线程。
  • 线程池上下文-如果不存在其他上下文(例如,ASP.NET Core和控制台应用程序),则为默认值。await在线程池线程上运行之后的代码。

🎏 9. 小结

嗯,本篇已完结。

例行小结,理性看待!

结的是啥啊,结的是我想你点赞而不可得的寂寞。😳😳😳

👓都看到这了,还在乎点个赞吗?

👓都点赞了,还在乎一个收藏吗?

👓都收藏了,还在乎一个评论吗?