There Is No Thread

这里并没有线程

原文地址: http://blog.stephencleary.com/2013/11/there-is-no-thread.html


最纯粹的async形式中存在一个重要的真相:这里并没有线程(或者不存在新建的线程)
举不胜数的反对者哭喊道:“不!如果我正在等待一个操作,那一定存在一个线程在等待这个操作!它可能是一个线程池中的线程。或者是系统线程!或者是其他类似设备驱动的东西…”。

不要听从那些哭喊的人。如果那些async操作是纯粹的,那么这里将不会存在线程。

那些持怀疑态度的人并没有被说服。让我们来娱乐一下他们。

我们可以一路跟踪一条异步操作指令到硬件层面,特别留意其中的.Net部分和设备驱动部分。为了简化这部分描述,我们排除掉部分中间层的细节,但是这应该不会让我们偏离真相。

思考一个通用的“写”操作(写一个文件、网络流、USB等等)。我们的代码很简单:

1
2
3
4
5
private async void Button_Clicked(object sender, RoutedEventArgs e)
{
byte[] data = ...
await myDevice.WriteAsync(data, 0, data.Length);
}

我们早就知道,UI线程并不会被await操作阻塞。那么问题来了:这里是不是存在另外一个线程,它牺牲自己所以UI线程才能存活?

抓住我的手,我们要潜的更深一点。

第一站:类库(例如,查看BCL代码)。我们假设 WriteAsync 是用 .Net 标准的 P/Invoke 异步 I/O 操作实现的。所以,这个操作在设备的句柄上开始了一个Win32的重叠I/O操作。

系统紧接着转到设备驱动并让设备开始写操作。它首先构造一个表示写请求的对象,这被称为I/O Request Packet(IRP)。设备驱动获得这个IRP并向设备发起命令来写对应的数据。如果设备支持Direct Memory Access(DMA),这个操作就像向设备寄存器中写入缓存地址一样简单。这是设备驱动能做的所有事情;它使得IRP进入“等待”并转回到系统。

事实的核心:当处理IRP时,设备驱动不允许堵塞。这意味着,如果IRP不能立即完成,那么它一定要异步执行。这对于同步API也一样成立。在设备驱动这一层,所有(重要的)请求都是异步的。
随着IRP进入“等待”状态,系统通过返回一个未完成的Task给刚才堵塞的async按钮点击事件,然后UI线程继续执行。
我们深入追踪到系统底层的写操作,直至物理设备。
现在,写操作正在执行,那有多少线程在处理它呢?

一个都没有

这里并没有设备驱动线程,系统线程,BLC线程或者线程池线程在处理那个写操作。这里根本就没有线程
现在,我们看一下对应的回复(Response)。
写请求开始片刻,设备完成了写操作,他通过中断(Interrupt)通知CPU。
设备驱动的Interrupt Service Routine(ISR)对这个中断做出反应。中断是CPU层的时间,它会临时从当前CPU所运行的线程中获得CPU的控制权。你可以认为ISR是在“借用”当前正在运行的线程,但是我更倾向于认为ISR在更底层中执行,底层到根本不存在线程这个概念的水平。或者说它在所有线程之下。
无论如何,ISR已经被妥当的进行了写操作,它做的所有事情就是告诉设备“谢谢你的中断请求”并且将一个Deferred Procedure Call(DPC)入队(queue)。
当CPU被中断“骚扰”完之后,它会转向它的DPC。DPC也是在一个不能直接用线程描述的底层水平。和ISR一样,DPC直接在CPU上执行,在线程系统之下。
DPC获取代表写操作的ISR并将其标志成“完成”。然而,那么“完成”状态只存在系统层;必须要通知到进程自己拥有的内存空间。所以系统会入队一个special-kernel-mode Asynchronous Procedure Call(APC)给拥有HANDLE的线程。
因为上述提到的类库/BLC使用的是标准P/Inovke Overlapped I/O 系统,它早就注册了I/O Completion Port(IOCP)的句柄,这个句柄是线程池的一部分。所以,一个I/O线程池线程被短暂的“借用”来执行APC,通过它来通知task已经完成。
现在task捕获了UI线程的上下文,它不直接从线程池线程中返回async方法,相反,它将async方法后续的执行加入到UI线程的上下文,当UI线程执行到这里的时候就会继续执行这部分代码。
所以,当请求发生时,我们可以看到这里并没有线程。当请求完成时,大量的线程被“借用”或者短暂暂存到它们那里。这些工作大约在一毫秒(运行在线程池的APC)或者低到一微秒(例如ISR)。但是这里并没有线程因为等待请求完毕而被堵塞。

评论