一个高频问题(异步操作会创建线程吗())
这个问题在微信上被别人问过好多次,想来想去觉得有必要统一解答下,先说下我的答案:可能会,也有可能不会。
要想寻找答案,需要从 异步处理
的底层框架说起。
一:异步底层是什么
异步
从设计层面上来说它就是一个 发布订阅者
模式,毕竟它的底层用到了 端口完成队列
,可以从 IO完成端口内核对象
所提供的三个方法中有所体现。
- CreateIoCompletionPort
HANDLE WINAPI CreateIoCompletionPort(
_In_HANDLEFileHandle,
_In_opt_ HANDLEExistingCompletionPort,
_In_ULONG_PTR CompletionKey,
_In_DWORDNumberOfConcurrentThreads
);
这个方法主要是将
文件句柄
和 IO完成端口内核对象
进行绑定,其中的 NumberOfConcurrentThreads
表示完成端口最多允许 running 的线程上限。- PostQueuedCompletionStatus
BOOL WINAPI PostQueuedCompletionStatus(
_In_HANDLECompletionPort,
_In_DWORDdwNumberOfBytesTransferred,
_In_ULONG_PTRdwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);
这个函数的作用就是将一个
包
通过 内核对象
丢给 驱动设备程序
,由后者与硬件交互,比如文件
。- GetQueuedCompletionStatus
BOOL GetQueuedCompletionStatus(
[in]HANDLECompletionPort,
LPDWORDlpNumberOfBytesTransferred,
[out] PULONG_PTRlpCompletionKey,
[out] LPOVERLAPPED *lpOverlapped,
[in]DWORDdwMilliseconds
);
这个方法尝试从
IO完成端口内核对象
中提取 IO 包,如果没有提取到,那么就会无限期等待,直到提取为止。对上面三个方法有了概念之后,接下来看下结构图:
文章图片
这张图非常言简意赅,不过只画了
端口完成队列
, 其实还有三个与IO线程有关的队列,分别为:等待线程队列
, 已释放队列
, 已暂停队列
,接下来我们稍微解读一下。当
线程t1
调用 GetQueuedCompletionStatus
时,假使此刻 任务队列q1
无任务, 那么 t1
会卡住并自动进去 等待线程队列
,当某个时刻 q1
进了任务(由驱动程序投递的),此时操作系统会将 t1
激活来提取 q1
的任务执行,同时将 t1
送到已释放队列
中。这个时候就有两条路了。
- 遇到 Sleep 或者 lock 情况。
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 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 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 逻辑。推荐阅读
- 面试题目:手写一个LRU算法实现
- 关于k8s|关于k8s 使用 Service 控制器对外暴露服务的问题
- 背包问题(3)(完全背包)
- 用一个文件,实现迷你|用一个文件,实现迷你 Web 框架
- stm32正常运行流程图_STM32单片机学习笔记(超详细整理143个问题,学习必看)...
- unity|微信SDK 接入xcode构建时,libiPhone-lib.a报错的问题
- nginx|Nginx获取真实用户IP
- 游戏|unity3d导出xcode项目使用afnetworking 3框架导致_kUTTagClassMIMEType 问题解决方案
- IT|iOS - 将Unity导出的Xcode工程导入到另一个Xcode项目, 及常见报错的解决方法
- java学习|java面试基础问题答不上来怎么办,快来看鸭~