这个问题在微信上被别人问过好多次,想来想去觉得有必要统一解答下,先说下我的答案:可能会,也有可能不会

要想寻找答案,需要从 ​​异步处理​​ 的底层框架说起。

一:异步底层是什么

​异步​​​ 从设计层面上来说它就是一个 ​​发布订阅者​​​ 模式,毕竟它的底层用到了 ​​端口完成队列​​​,可以从 ​​IO完成端口内核对象​​ 所提供的三个方法中有所体现。

  1. CreateIoCompletionPort

可以粗看下签名:

HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);

这个方法主要是将 ​​文件句柄​​​ 和 ​​IO完成端口内核对象​​​ 进行绑定,其中的 ​​NumberOfConcurrentThreads​​ 表示完成端口最多允许 running 的线程上限。

  1. PostQueuedCompletionStatus

再看签名:

BOOL WINAPI PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNumberOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);

这个函数的作用就是将一个 ​​包​​​ 通过 ​​内核对象​​​ 丢给 ​​驱动设备程序​​​ ,由后者与硬件交互,比如​​文件​​。

  1. GetQueuedCompletionStatus

看签名:

BOOL GetQueuedCompletionStatus(
[in] HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
[out] PULONG_PTR lpCompletionKey,
[out] LPOVERLAPPED *lpOverlapped,
[in] DWORD dwMilliseconds
);

这个方法尝试从 ​​IO完成端口内核对象​​ 中提取 IO 包,如果没有提取到,那么就会无限期等待,直到提取为止。

对上面三个方法有了概念之后,接下来看下结构图:

一个高频问题:异步操作会创建线程吗?_内核对象

这张图非常言简意赅,不过只画了 ​​端口完成队列​​​, 其实还有三个与IO线程有关的队列,分别为:​​等待线程队列​​​, ​​已释放队列​​​, ​​已暂停队列​​,接下来我们稍微解读一下。

当 ​​线程t1​​​ 调用 ​​GetQueuedCompletionStatus​​​ 时,假使此刻 ​​任务队列q1​​​ 无任务, 那么 ​​t1​​​ 会卡住并自动进去 ​​等待线程队列​​​ ,当某个时刻 ​​q1​​​ 进了任务(由驱动程序投递的),此时操作系统会将 ​​t1​​​ 激活来提取 ​​q1​​​ 的任务执行,同时将 ​​t1​​​ 送到​​已释放队列​​中。

这个时候就有两条路了。

  1. 遇到 Sleep 或者 lock 情况。

如果 t1 在执行的时候,遇到了 ​​Sleep​​​ 或者 ​​lock​​​ 锁时需要被迫停止,此时系统会将 t1 线程送到 ​​已暂停线程队列​​​ 中,如果都 sleep 了,那 ​​NumberOfConcurrentThreads​​​ 就会变为 0 ,此时就会遇到无人可用的情况,那怎么办呢?只能让系统从 ​​线程池​​​ 中申请更多的线程来从 ​​q1​​​ 队列中提取任务,当某个时刻, ​​已暂停线程队列​​​ 中的线程激活,那么它又回到了 ​​已释放队列​​​ 中继续执行任务,当任务执行完之后,再次调用 ​​GetQueuedCompletionStatus​​​ 方法进去 ​​等待线程队列​​。

当然这里有一个问题,某一个时刻 ​​等待线程队列​​​ 中的线程数会暂时性的超过 ​​NumberOfConcurrentThreads​​ 值,不过问题也不大。

说了这么多理论是不是有点懵, 没关系,接下来我结合 windbg 和 coreclr 源码一起看下。

一个高频问题:异步操作会创建线程吗?_内核对象_02

以我的机器来说,​​IO完成端口内核对象​​​ 默认最多允许 ​​12​​​ 个 running 线程,当遇到 sleep 时看看会不会突破 ​​12​​ 的限制,上代码:

class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 2000; i++)
{
Task.Run(async () =>
{
await GetString();
});
}

Console.ReadLine();
}

public static int counter = 0;

static async Task<string> GetString()
{
var httpClient = new HttpClient();

var str = await httpClient.GetStringAsync("http://cnblogs.com");

Console.WriteLine($"counter={++counter}, 线程:{Thread.CurrentThread.ManagedThreadId},str.length={str.Length}");

Thread.Sleep(1000000);

return str;
}
}

一个高频问题:异步操作会创建线程吗?_创建线程_03

从图中看,已经破掉了 ​​12​​ 的限制,那是不是 30 呢? 可以用 windbg 帮忙确认一下。

0:059> !tp
CPU utilization: 3%
Worker Thread: Total: 13 Running: 0 Idle: 13 MaxLimit: 2047 MinLimit: 12
Work Request in Queue: 0
--------------------------------------
Number of Timers: 1
--------------------------------------
Completion Port Thread:Total: 30 Free: 0 MaxFree: 24 CurrentLimit: 30 MaxLimit: 1000 MinLimit: 12

从最后一行看,没毛病, ​​IO完成端口线程​​​ 确实是 ​​30​​ 个。


在这种情况,异步操作一定会创建线程来处理


  1. 遇到耗时操作

所谓的耗时操作,大体上是大量的序列化,复杂计算等等,这里我就用 ​​while(true)​​​ 模拟,因为所有线程都没有遇到暂停事件,所以理论上不会突破 ​​12​​​ 的限制,接下来稍微修改一下 ​​GetString()​​ 方法。

static async Task<string> GetString()
{
var httpClient = new HttpClient();

var str = await httpClient.GetStringAsync("http://cnblogs.com");

Console.WriteLine($"counter={++counter},时间:{DateTime.Now}, 线程:{Thread.CurrentThread.ManagedThreadId},str.length={str.Length}");

while (true) { }

return str;
}

一个高频问题:异步操作会创建线程吗?_windbg_04

对比图中的时间,过了30s也无法突破 12 的限制,毕竟这些线程都是 running 状态并都在 ​​已释放队列​​​中,这也就造成了所谓的 ​​请求无响应​​ 的尴尬情况。

二:直面问题

如果明白了上面我所说的,那么 ​​异步操作会不会创建线程 ?​​​ 问题,我的答案是 ​​有可能会也有可能不会​​,具体还是取决于上面提到了两种 callback 逻辑。

一个高频问题:异步操作会创建线程吗?_创建线程_05