原文来自:https://blog.stephencleary.com/2013/11/there-is-no-thread.html
作者:Stephen Cleary
翻译 by abaabaqua
这是最纯粹形式的异步的一个基本事实:没有线程。
反对这一事实的人很多。”不”,他们喊道,”如果我正在等待手术,一定有一个线程在等待!它可能是一个线程池线程。或者操作系统线程!或者带有设备驱动程序的东西……”
不要理会那些哭声。如果异步操作是纯粹的,那么就没有线程。
这是最纯粹形式的异步的一个基本事实:没有线程。
反对这一事实的人很多。”不”,他们喊道,”如果我正在等待手术,一定有一个线程在等待!它可能是一个线程池线程。或者操作系统线程!或者带有设备驱动程序的东西……”
不要理会那些哭声。如果异步操作是纯粹的,那么就没有线程。
持怀疑态度的人并不相信。让我们迁就他们一下。
我们将跟踪异步操作一直到硬件,特别注意 .NET 部分和设备驱动程序部分。我们必须通过省略一些中间层细节来简化这种描述,但我们不会偏离事实。
考虑一个通用的“写入”操作(写入到文件、网络流、USB 烤面包机等)。我们的代码很简单:
1 | private async void Button_Click(object sender, RoutedEventArgs e) |
我们已经知道UI线程在await
期间不会被阻塞。问题是是否会有另一个线程必须在阻塞的祭坛上牺牲自己,以便UI线程可以存活?
抓住我的手。我们将深入研究。
第一站:库(例如,进入BCL的代码)。让我们假设WriteAsync
是使用.NET中的标准P/Invoke 异步I/O系统,该系统基于overlapped I/O。所以这个操作会在设备底层的HANDLE
上启动一个Win32的overlapped I/O。
然后操作系统转向设备驱动程序并要求它开始进行写操作。这个操作会首先构造一个表示写请求的对象;这被称为I/O请求数据包(I/O Request Packet,IRP)。
设备驱动程序接收到IRP并向设备发出写数据的命令。如果设备支持直接内存访问 (DMA),这就像将缓冲区地址写入设备寄存器一样简单。这就是设备驱动程序所能做的;它将IRP标记为”pending”并返回到操作系统那层。
事实的核心在这里找到:设备驱动程序在处理IRP时不允许阻塞。这意味着如果IRP不能被立即完成,那么它必须被异步处理。即使对于同步API也是如此!在设备驱动程序级别,所有(重要的)请求都是异步的。
引用 Tomes of Knowledge的话,”不管是什么类型的I/O请求,向驱动程序发出的代表应用程序的内部I/O操作都是异步执行的”。
随着IRP”pending”,操作系统返回到库,库将未完成的任务返回给按钮单击事件handler,该handler挂起异步方法,UI线程继续执行。
我们已经按照请求深入到系统的深渊,一直到物理设备。
写操作现在”进行中”。那有多少线程正在处理它?
答案是没有。
没有设备驱动程序线程、OS线程、BCL线程或线程池线程处理该写操作。没有线程。
现在,让我们随着内核恶魔之地的回应回到凡人世界。
在写入请求开始后的某个时间,设备完成了写入并通过中断通知CPU。
设备驱动程序的中断服务例程(Interrupt Service Routine,ISR)响应中断。中断是CPU级别的事件,会暂时从正在运行的线程中夺走对CPU的控制。您可以将ISR视为”借用”当前运行的线程,但我更倾向于将ISR理解为在如此低的级别上执行,以至于”线程”的概念不存在——因此可以说它们是位于所有线程的”下方”的线程。
ISR所做的只是告诉设备”谢谢你的中断”并将延迟过程调用(Deferred Procedure Call,DPC)放入队列。
当CPU不被中断打扰时,它将一一处理DPC。DPC的也在很低的层次上被执行,以至于说”线程”并不完全正确;与ISR一样,DPC直接在线程系统”之下”的CPU上执行。
DPC获取代表写入请求的IRP并将其标记为”完成”。然而,”完成”状态仅存在于操作系统级别;进程有着自己的内存空间,必须被通知操作已经完成。因此,操作系统将一个特殊内核模式异步过程调用 (Asynchronous Procedure Call, APC) 放入到拥有HANDLE
的线程的队列。
由于BCL库使用标准的P/Invoke overlapped I/O系统,它已经通过I/O完成端口(I/O Completion Port, IOCP)注册了handler,IOCP是线程池的一部分。因此,一个I/O线程池线程被短暂借用来执行APC,APC会通知任务已完成。
该任务已经捕获了UI上下文,因此它不会直接在线程池的线程上继续async
方法。相反,它将该方法的continuation放入UI上下文的队列中,并且UI线程将在之后恢复执行该方法。
因此,我们看到在写请求运行期间没有线程参与。只有当请求完成时,各种线程被”借用”或暂时将工作放入队列。这项工作通常在毫秒左右(例如,线程池上运行的APC)到微秒左右(例如,ISR)。但是没有线程被阻塞,只是在等待该请求完成。
现在,我们遵循的是”标准化”的路径并进行了一些简化。有无数的变种,但核心事实是一样的。
“在某处必须有一个线程在处理异步操作”的想法是不正确的。
解放你的思想。不要试图找到这个”异步线程”——这是不可能的。相反,只需尝试了解一下真相:
没有线程
原文来自:https://blog.stephencleary.com/2013/11/there-is-no-thread.html
作者:Stephen Cleary
翻译 by abaabaqua