异步操作是否等同于创建线程执行?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1933个文字,预计阅读时间需要8分钟。
这个问题的微信上被问过很多次,大家都很想统一一下答案。下面先说我的观点:可能会,也可能不会。如果想找到答案,需要从异步处理的底层框架说起。
一、异步处理的底层框架
1. 事件循环(Event Loop)事件循环是异步编程的核心机制,它允许程序在等待某些操作完成时继续执行其他任务。在事件循环中,程序会不断检查是否有事件(如IO操作、定时器等)触发,然后执行相应的事件处理函数。
2. PromisePromise是JavaScript中用于处理异步操作的一种对象。它代表了一个异步操作的最终完成(或失败)及其结果值。Promise有三种状态:pending(等待态)、fulfilled(成功态)和rejected(失败态)。
3. async/awaitasync/await是ES2017引入的语法糖,用于简化异步代码的编写。它允许开发者以同步的方式编写异步代码,使得异步编程更加直观和易读。
二、异步处理的优点
1. 提高程序性能异步处理可以避免阻塞主线程,从而提高程序性能。
2. 增强用户体验异步处理可以使程序在等待某些操作完成时,继续执行其他任务,从而提高用户体验。
3. 代码结构清晰异步处理可以使代码结构更加清晰,易于维护。
总结:异步处理在提高程序性能、增强用户体验和代码结构方面具有明显优势。在实际开发中,应根据具体需求选择合适的异步处理方式。
这个问题在微信上被别人问过好多次,想来想去觉得有必要统一解答下,先说下我的答案:可能会,也有可能不会。
要想寻找答案,需要从 异步处理 的底层框架说起。
异步 从设计层面上来说它就是一个 发布订阅者 模式,毕竟它的底层用到了 端口完成队列,可以从 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 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 cnblogs.com");
Console.WriteLine($"counter={++counter},时间:{DateTime.Now}, 线程:{Thread.CurrentThread.ManagedThreadId},str.length={str.Length}");
while (true) { }
return str;
}
对比图中的时间,过了30s也无法突破 12 的限制,毕竟这些线程都是 running 状态并都在 已释放队列中,这也就造成了所谓的 请求无响应 的尴尬情况。
如果明白了上面我所说的,那么 异步操作会不会创建线程 ? 问题,我的答案是 有可能会也有可能不会,具体还是取决于上面提到了两种 callback 逻辑。
本文共计1933个文字,预计阅读时间需要8分钟。
这个问题的微信上被问过很多次,大家都很想统一一下答案。下面先说我的观点:可能会,也可能不会。如果想找到答案,需要从异步处理的底层框架说起。
一、异步处理的底层框架
1. 事件循环(Event Loop)事件循环是异步编程的核心机制,它允许程序在等待某些操作完成时继续执行其他任务。在事件循环中,程序会不断检查是否有事件(如IO操作、定时器等)触发,然后执行相应的事件处理函数。
2. PromisePromise是JavaScript中用于处理异步操作的一种对象。它代表了一个异步操作的最终完成(或失败)及其结果值。Promise有三种状态:pending(等待态)、fulfilled(成功态)和rejected(失败态)。
3. async/awaitasync/await是ES2017引入的语法糖,用于简化异步代码的编写。它允许开发者以同步的方式编写异步代码,使得异步编程更加直观和易读。
二、异步处理的优点
1. 提高程序性能异步处理可以避免阻塞主线程,从而提高程序性能。
2. 增强用户体验异步处理可以使程序在等待某些操作完成时,继续执行其他任务,从而提高用户体验。
3. 代码结构清晰异步处理可以使代码结构更加清晰,易于维护。
总结:异步处理在提高程序性能、增强用户体验和代码结构方面具有明显优势。在实际开发中,应根据具体需求选择合适的异步处理方式。
这个问题在微信上被别人问过好多次,想来想去觉得有必要统一解答下,先说下我的答案:可能会,也有可能不会。
要想寻找答案,需要从 异步处理 的底层框架说起。
异步 从设计层面上来说它就是一个 发布订阅者 模式,毕竟它的底层用到了 端口完成队列,可以从 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 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 cnblogs.com");
Console.WriteLine($"counter={++counter},时间:{DateTime.Now}, 线程:{Thread.CurrentThread.ManagedThreadId},str.length={str.Length}");
while (true) { }
return str;
}
对比图中的时间,过了30s也无法突破 12 的限制,毕竟这些线程都是 running 状态并都在 已释放队列中,这也就造成了所谓的 请求无响应 的尴尬情况。
如果明白了上面我所说的,那么 异步操作会不会创建线程 ? 问题,我的答案是 有可能会也有可能不会,具体还是取决于上面提到了两种 callback 逻辑。

