在没有 UI 线程的专用后端服务器上使用异步代码是否有优势?
Are there advantages to asynchronous code on dedicated backend servers with no UI thread?
前几天我有一个开发人员挑战异步服务器端代码的使用。他问为什么在没有 UI 线程阻塞的服务器上异步代码优于同步代码?我给了他典型的线程耗尽答案,但在考虑了一段时间后,我不再确定我的答案是否正确。在做了一些研究之后,我发现 OS 中线程的上限是由内存而不是任意数字决定的。像 Kestrel 这样的服务器支持无限线程。所以在“理论上”服务器可以并行阻塞的请求(线程)数量由内存控制。这与 .NET 中的异步代码没有什么不同;它将堆栈变量提升到堆中,但它仍然受内存限制。
我一直认为比我聪明的人已经深思熟虑,异步代码是处理 IO 绑定代码的正确方法。但是,当 运行 在没有 UI 线程的专用服务器场中时,异步 .NET 代码的可衡量优势是什么?迁移到云端 (AWS) 会改变答案吗?
Server-side 异步代码的目的与异步 UI 代码完全不同。
异步 UI 代码使 UI 响应更快(尤其是当有多个 CPU 内核可用时),它允许多个 UI 任务并行 运行,这提高了 UI 用户体验。
另一方面,server-side 异步代码的目的是 最大限度地减少同时服务多个客户端所需的资源 。事实上,即使只有一个 CPU 核心或像 Node.js 中那样的 single-threaded 事件循环,它也是有益的。这一切都归结为一个简单的概念
Asynchronous IO.
同步IO和异步IO的区别在于,前者初始化IO操作的线程暂停,直到IO操作完成(例如,直到执行DB请求或读取磁盘上的文件) .一旦 IO 操作完成,同一个线程就会 un-paused 处理它的结果。注意:即使在暂停时线程很可能没有使用任何 CPU 资源(它可能被线程调度程序置于睡眠状态)它的资源仍然与这个特定的 IO 操作相关联并且在 IO 时几乎被浪费了由硬件执行。有效地使用同步 IO,您将需要至少一个线程来处理每个当前正在处理的客户端请求,即使这些线程中的大多数可能正在等待其 IO 操作完成。在 .NET 中,每个线程至少分配了 1MB 的堆栈,因此如果服务器当前正在处理 1000 个请求,则会导致为线程堆栈分配近 1GB 的内存,加上线程调度程序的额外负担和更多时间 CPU花费做上下文切换:线程越多,系统的整体性能越慢。分配的内存越多,意味着缓存使用效率越低 memory/CPU。
异步IO效率更高,因为工作线程只初始化一个IO操作,而不是等待它完成它立即切换到另一个有用的任务(例如继续另一个客户端的请求处理),当IO操作是由硬件完成,结果的处理在任何可用的工作线程上恢复。因此,根据等待硬件完成 IO 所花费的总时间与执行 CPU 任务所花费时间(例如,将 IO 操作的结果序列化为 JSON)之间的比率,这种方法可以使用 更少的线程来服务相同数量的并发客户端请求:如果,比方说,90% 的时间花在 IO 上,我们可能只使用 100 个线程来服务相同的 1000 个并发请求。您的 server-side 代码越 IO-bound 与 CPU-bound 相比,它可以使用给定数量的资源处理的并发客户端请求越多:CPU 和内存。
异步代码的缺点是什么?主要是它通常比同步更难写。异步代码使用回调来恢复操作,因此程序员需要将委托(延续)传递给 IO 方法,而不是简单的线性代码,该方法稍后在 IO 操作完成时由系统调用(可能在不同的线程上)。然而,现代 C# 及其 async/await
设施使这项任务变得不那么复杂,甚至使异步代码看起来几乎像同步代码。唯一要记住的是:异步代码只有在“一直向下”异步时才能工作:即使是单个 Task.Wait or Task.Result somewhere in the stack of calls from initial HTTP request processing to DB request call makes entire code synchronous thus forcing the current working thread to wait for that Wait
call to finish defeating the purpose. Note: await
in C# code does not actually awaits to the result of the call but is converted by the compiler to a ContinueWith i.e. to a continuation callback though in practice it is a bit more complicated than that 但幸运的是,程序员隐藏了复杂性,因此如今编写高效的异步代码是相对简单的任务。
前几天我有一个开发人员挑战异步服务器端代码的使用。他问为什么在没有 UI 线程阻塞的服务器上异步代码优于同步代码?我给了他典型的线程耗尽答案,但在考虑了一段时间后,我不再确定我的答案是否正确。在做了一些研究之后,我发现 OS 中线程的上限是由内存而不是任意数字决定的。像 Kestrel 这样的服务器支持无限线程。所以在“理论上”服务器可以并行阻塞的请求(线程)数量由内存控制。这与 .NET 中的异步代码没有什么不同;它将堆栈变量提升到堆中,但它仍然受内存限制。
我一直认为比我聪明的人已经深思熟虑,异步代码是处理 IO 绑定代码的正确方法。但是,当 运行 在没有 UI 线程的专用服务器场中时,异步 .NET 代码的可衡量优势是什么?迁移到云端 (AWS) 会改变答案吗?
Server-side 异步代码的目的与异步 UI 代码完全不同。
异步 UI 代码使 UI 响应更快(尤其是当有多个 CPU 内核可用时),它允许多个 UI 任务并行 运行,这提高了 UI 用户体验。
另一方面,server-side 异步代码的目的是 最大限度地减少同时服务多个客户端所需的资源 。事实上,即使只有一个 CPU 核心或像 Node.js 中那样的 single-threaded 事件循环,它也是有益的。这一切都归结为一个简单的概念
Asynchronous IO.
同步IO和异步IO的区别在于,前者初始化IO操作的线程暂停,直到IO操作完成(例如,直到执行DB请求或读取磁盘上的文件) .一旦 IO 操作完成,同一个线程就会 un-paused 处理它的结果。注意:即使在暂停时线程很可能没有使用任何 CPU 资源(它可能被线程调度程序置于睡眠状态)它的资源仍然与这个特定的 IO 操作相关联并且在 IO 时几乎被浪费了由硬件执行。有效地使用同步 IO,您将需要至少一个线程来处理每个当前正在处理的客户端请求,即使这些线程中的大多数可能正在等待其 IO 操作完成。在 .NET 中,每个线程至少分配了 1MB 的堆栈,因此如果服务器当前正在处理 1000 个请求,则会导致为线程堆栈分配近 1GB 的内存,加上线程调度程序的额外负担和更多时间 CPU花费做上下文切换:线程越多,系统的整体性能越慢。分配的内存越多,意味着缓存使用效率越低 memory/CPU。
异步IO效率更高,因为工作线程只初始化一个IO操作,而不是等待它完成它立即切换到另一个有用的任务(例如继续另一个客户端的请求处理),当IO操作是由硬件完成,结果的处理在任何可用的工作线程上恢复。因此,根据等待硬件完成 IO 所花费的总时间与执行 CPU 任务所花费时间(例如,将 IO 操作的结果序列化为 JSON)之间的比率,这种方法可以使用 更少的线程来服务相同数量的并发客户端请求:如果,比方说,90% 的时间花在 IO 上,我们可能只使用 100 个线程来服务相同的 1000 个并发请求。您的 server-side 代码越 IO-bound 与 CPU-bound 相比,它可以使用给定数量的资源处理的并发客户端请求越多:CPU 和内存。
异步代码的缺点是什么?主要是它通常比同步更难写。异步代码使用回调来恢复操作,因此程序员需要将委托(延续)传递给 IO 方法,而不是简单的线性代码,该方法稍后在 IO 操作完成时由系统调用(可能在不同的线程上)。然而,现代 C# 及其 async/await
设施使这项任务变得不那么复杂,甚至使异步代码看起来几乎像同步代码。唯一要记住的是:异步代码只有在“一直向下”异步时才能工作:即使是单个 Task.Wait or Task.Result somewhere in the stack of calls from initial HTTP request processing to DB request call makes entire code synchronous thus forcing the current working thread to wait for that Wait
call to finish defeating the purpose. Note: await
in C# code does not actually awaits to the result of the call but is converted by the compiler to a ContinueWith i.e. to a continuation callback though in practice it is a bit more complicated than that 但幸运的是,程序员隐藏了复杂性,因此如今编写高效的异步代码是相对简单的任务。