这个问题在微信上被别人问过好多次,想来想去觉得有必要统一解答下,先说下我的答案:可能会,也有可能不会。
要想寻找答案,需要从异步处理
的底层框架说起。
一:异步底层是什么
异步
从设计层面上来说它就是一个发布订阅者
模式,毕竟它的底层用到了端口完成队列
,可以从IO完成端口内核对象
所提供的三个方法中有所体现。
- CreateIoCompletionPort
可以粗看下签名:
HANDLE WINAPI CreateIoCompletionPort( _In_ HANDLE FileHandle, _In_opt_ HANDLE ExistingCompletionPort, _In_ ULONG_PTR CompletionKey, _In_ DWORD NumberOfConcurrentThreads );
这个方法主要是将文件句柄
和IO完成端口内核对象
进行绑定,其中的NumberOfConcurrentThreads
表示完成端口最多允许 running 的线程上限。
- PostQueuedCompletionStatus
再看签名:
BOOL WINAPI PostQueuedCompletionStatus( _In_ HANDLE CompletionPort, _In_ DWORD dwNumberOfBytesTransferred, _In_ ULONG_PTR dwCompletionKey, _In_opt_ LPOVERLAPPED lpOverlapped );
这个函数的作用就是将一个包
通过内核对象
丢给驱动设备程序
,由后者与硬件交互,比如文件
。
- 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
送到已释放队列
中。
这个时候就有两条路了。
- 遇到 Sleep 或者 lock 情况。
如果 t1 在执行的时候,遇到了Sleep
或者lock
锁时需要被迫停止,此时系统会将 t1 线程送到已暂停线程队列
中,如果都 sleep 了,那NumberOfConcurrentThreads
就会变为 0 ,此时就会遇到无人可用的情况,那怎么办呢?只能让系统从线程池
中申请更多的线程来从q1
队列中提取任务,当某个时刻,已暂停线程队列
中的线程激活,那么它又回到了已释放队列
中继续执行任务,当任务执行完之后,再次调用GetQueuedCompletionStatus
方法进去等待线程队列
。
当然这里有一个问题,某一个时刻等待线程队列
中的线程数会暂时性的超过NumberOfConcurrentThreads
值,不过问题也不大。
说了这么多理论是不是有点懵, 没关系,接下来我结合 windbg 和 coreclr 源码一起看下。
以我的机器来说,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; } }
从图中看,已经破掉了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
个。
在这种情况,异步操作一定会创建线程来处理
- 遇到耗时操作
所谓的耗时操作,大体上是大量的序列化,复杂计算等等,这里我就用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; }
对比图中的时间,过了30s也无法突破 12 的限制,毕竟这些线程都是 running 状态并都在已释放队列
中,这也就造成了所谓的请求无响应
的尴尬情况。
二:直面问题
如果明白了上面我所说的,那么异步操作会不会创建线程 ?
问题,我的答案是有可能会也有可能不会
,具体还是取决于上面提到了两种 callback 逻辑。
热门文章
- 3月12日 | Clash Verge每天更新19M/S免费节点订阅链接地址
- 送养协议怎么写才合法一点(送养协议怎么写才合法一点呢)
- 动物疫苗类型有哪几种(动物疫苗种类及类型)
- 纹绣美甲美睫培训学校哪家好(领绣集团纹绣培训学院)
- BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor以及BeanPostProcessor执行先后顺序
- 动物诊疗许可证办理流程视频教程(动物诊疗许可证申请条件)
- 3月27日 | Clash Verge每天更新23M/S免费节点订阅链接地址
- 4月1日 | Clash Verge每天更新22.5M/S免费节点订阅链接地址
- 3月15日 | Clash Verge每天更新18.5M/S免费节点订阅链接地址
- 3月8日 | Clash Verge每天更新19.4M/S免费节点订阅链接地址