非阻塞 I/O 真的比多线程阻塞 I/O 快吗? 怎么样?

我在网上搜索了一些关于阻塞 I/O 和非阻塞 I/O 的技术细节,发现有几个人声称非阻塞 I/O 比阻塞 I/O 更快,例如在 这份文件中。

如果我使用阻塞 I/O,那么当前被阻塞的线程当然不能做任何其他事情... 因为它被阻塞了。但是,一旦一个线程开始被阻塞,操作系统可以切换到另一个线程,而不切换回来,直到有一些事情要做的阻塞线程。因此,只要系统上还有另一个线程需要 CPU 并且没有被阻塞,那么与基于事件的非阻塞方法相比,就不应该有更多的 CPU 空闲时间,不是吗?

除了减少 CPU 的空闲时间外,我还看到了一个增加计算机在给定时间框架内可以执行的任务数量的选项: 减少切换线程所带来的开销。但如何才能做到这一点呢?开销是否足够大,以显示可测量的影响?这里有一个关于我如何描绘它工作的想法:

  1. 为了加载文件的内容,应用程序将此任务委托给一个基于事件的 i/o 框架,同时传递一个回调函数和一个文件名
  2. 事件框架委托给操作系统,操作系统对硬盘的 DMA 控制器进行编程,将文件直接写入内存
  3. 事件框架允许进一步的代码运行。
  4. 在完成磁盘到内存的副本后,DMA 控制器会导致中断。
  5. 操作系统的 interrupt handler 通知基于事件的 i/o 框架文件已完全加载到内存中。它是怎么做到的?用信号?
  6. 当前在事件 i/o 框架中运行的代码完成。
  7. 基于事件的 i/o 框架检查其队列,查看步骤5中的操作系统消息,并执行步骤1中获得的回调。

是这样吗?如果没有,它是如何工作的?这意味着事件系统可以在不需要显式触及堆栈的情况下工作(例如,一个真正的调度程序需要备份堆栈,并在切换线程时将另一个线程的堆栈复制到内存中) ?这到底能节省多少时间?还有别的吗?

46494 次浏览

据我所知,改进是异步 I/O 使用(我说的是 MS System,只是为了澄清) so 称为 I/O 完成端口。通过使用异步调用,框架可以自动利用这样的体系结构,这应该比标准的线程机制更有效率。作为个人经验,我可以说,如果您更喜欢 AsyncCall 而不是阻塞线程,那么您会明智地感觉到应用程序的反应性更强。

非阻塞 I/O 的一个可能的实现就是您所说的,使用一个执行阻塞 I/O 的后台线程池,并通过某种回调机制通知发起者的线程 I/O。实际上,glibc 中的 AIO模块就是这样工作的。给你是一些关于实现的模糊细节。

虽然这是一个非常可移植的很好的解决方案(只要您有线程) ,但是操作系统通常能够更有效地服务非阻塞 I/O。这篇维基百科的文章列出了除线程池之外的可能实现。

非阻塞或异步 I/O 的最大优点是线程可以并行地继续它的工作。当然,您也可以使用一个额外的线程来实现这一点。正如您所说的,为了获得最佳的整体(系统)性能,我想最好使用异步 I/O 而不是多线程(这样可以减少线程切换)。

让我们看看一个网络服务器程序的可能实现,它可以处理并行连接的1000个客户端:

  1. 每个连接一个线程(可以阻塞 I/O,但也可以是非阻塞 I/O)。
    每个线程都需要内存资源(也是内核内存!),这是一个劣势。每个额外的线程都意味着调度程序要做更多的工作。
  2. 一个线程用于所有连接。
    这将从系统中接受负载,因为我们有更少的线程。但是它也阻止您使用您的计算机的全部性能,因为您最终可能会将一个处理器的性能提高到100% ,并让所有其他处理器都处于空闲状态。
  3. 一些线程,其中每个线程处理一些连接。
    这将从系统中接受负载,因为线程较少。它可以使用所有可用的处理器。在 Windows 上,线程池 API支持这种方法。

当然,拥有更多的线程本身并不是一个问题。正如您可能已经认识到的,我选择了相当多的连接/线程。如果我们讨论的只是十几个线程(Raymond Chen 在 MSDN 的博客文章 Windows 是否有每个进程2000个线程的限制?中也提到了这一点) ,那么我怀疑您会看到这三种可能的实现之间有什么不同。

在 Windows 上使用 未缓冲文件 I/O意味着写操作的大小必须是页面大小的倍数。我还没有对它进行测试,但是听起来这也可能对缓冲的同步和异步写操作的写性能产生积极的影响。

您所描述的步骤1到7很好地说明了它是如何工作的。在 Windows 上,操作系统将通知您使用事件或回调完成异步 I/O (WriteFileOVERLAPPED结构)。例如,只有当您的代码调用 WaitForMultipleObjectsEx并且 bAlertable设置为 true时,才会调用回调函数。

网上更多阅读资料:

  • MSDN 上的用户界面 中的多线程,也可以很快处理创建线程的成本
  • 线程和线程池部分说: “尽管线程相对容易创建和使用,但操作系统分配了大量的时间和其他资源来管理它们。”
  • MSDN 上的 CreateThread 文档说: “然而,如果您为每个处理器创建一个线程并构建请求队列,而应用程序为其维护上下文信息,那么您的应用程序将具有更好的性能。”.
  • 旧文章 为什么太多的线程会影响性能

使用 AIO 的主要原因是可伸缩性。在几个线程的上下文中查看时,好处并不明显。但是当系统扩展到1000个线程时,AIO 将提供更好的性能。需要注意的是,AIO 库不应该引入进一步的瓶颈。

I/O 包括多种操作,如从硬盘读写数据、访问网络资源、调用 Web 服务或从数据库检索数据。根据平台和操作类型的不同,异步 I/O 通常会利用任何硬件或低级系统支持来执行操作。这意味着它将在对 CPU 影响尽可能小的情况下执行。

在应用程序级,异步 I/O 可以防止线程等待 I/O 操作完成。一旦启动了异步 I/O 操作,它就会释放启动它的线程,并注册一个回调。操作完成后,回调将在第一个可用线程上排队执行。

如果 I/O 操作是同步执行的,那么它会保持正在运行的线程不做任何事情,直到操作完成。运行时不知道 I/O 操作何时完成,因此它会周期性地为等待的线程提供一些 CPU 时间,否则这些 CPU 时间可能会被其他需要执行实际 CPU 绑定操作的线程使用。

因此,正如@user1629468所提到的,异步 I/O 并没有提供更好的性能,而是提供了更好的可伸缩性。当运行在可用线程数量有限的上下文中时,这是显而易见的,就像 Web 应用程序的情况一样。Web 应用程序通常使用一个线程池,它们从这个池中为每个请求分配线程。如果请求在长时间运行的 I/O 操作中被阻塞,就有可能耗尽网络池,使网络应用程序冻结或缓慢响应。

我注意到,在处理非常快速的 I/O 操作时,异步 I/O 并不是最佳选择。在这种情况下,在等待 I/O 操作完成时不让线程保持忙碌的好处并不十分重要,而且操作在一个线程上启动并在另一个线程上完成的事实会增加整体执行的开销。

您可以阅读我最近关于异步 I/O 与多线程 给你主题的更详细的研究。

我目前正在使用原型线程在嵌入式平台上实现异步。非阻塞输入输出在16000fps 和160fps 之间运行的区别。非阻塞 IO 的最大好处是,您可以在硬件执行任务的同时,将代码结构化为执行其他任务。甚至设备的初始化也可以并行完成。

马丁

假设由于任何形式的多计算而提高了速度,你必须假设多个基于 CPU 的任务在多个计算资源(通常是处理器核心)上并发执行,或者不是所有的任务都依赖于同一资源的并发使用——也就是说,一些任务可能依赖于一个系统子组件(比如磁盘存储) ,而一些任务依赖于另一个(从外围设备接收通信) ,还有一些任务可能需要使用处理器核心。

第一种情况通常称为“并行”编程。第二种情况通常被称为“并发”或“异步”编程,尽管“并发”有时也被用来指仅仅允许一个操作系统交错执行多个任务的情况,不管这种执行是否必须串行发生,或者是否可以使用多个资源来实现并行执行。在后一种情况下,“并发”通常指的是在程序中编写执行的方式,而不是从任务执行的实际同时性的角度来看。

说起这一切很容易就带着一种心照不宣的假设。例如,有些人很快声称“异步 I/O 将比多线程 I/O 更快”,这种说法可疑的原因有几个。首先,一些给定的异步 I/O 框架可以精确地用多线程实现,在这种情况下,它们是同一个线程,说一个概念“比另一个快”是没有意义的。

其次,即使有异步框架的单线程实现(例如单线程事件循环) ,您仍然必须假设该循环正在做什么。例如,对于单线程事件循环,可以做的一件傻事是请求它异步完成两个不同的纯 CPU 绑定任务。如果你在一台只有一个理想化的单处理器核心的机器上执行这个任务(忽略现代硬件优化) ,那么“异步”执行这个任务与执行两个独立管理的线程,或者只执行一个单独的进程没有什么不同——差异可能归结为线程上下文切换或操作系统调度优化,但如果两个任务都进入 CPU,那么两种情况都是相似的。

想象一下你可能遇到的许多不寻常或愚蠢的角落案例是很有用的。

“异步”不一定是并发的,例如: 您“异步”在一台只有一个处理器核心的机器上执行两个 CPU 绑定的任务。

多线程执行不必是并发的: 您在一台具有单个处理器核心的机器上产生两个线程,或者请求两个线程获取任何其他类型的稀缺资源(假设,一个网络数据库一次只能建立一个连接)。线程的执行可能是 交错出现,但是操作系统调度程序认为合适,但是它们的总运行时间不能减少(并且会从线程上下文切换中增加)在一个单一的核心上(或者更一般地说,如果你产生的线程比有更多的核心运行它们,或者有更多的线程请求一个资源比资源能够维持的)。同样的情况也适用于多处理。

因此,无论是异步 I/O 还是多线程都不必在运行时方面提供任何性能增益。他们甚至可以让事情变慢。

然而,如果你定义了一个特定的用例,比如一个特定的程序,它既发出一个网络调用来从网络连接的资源(比如远程数据库)中检索数据,又执行一些本地的 CPU 绑定计算,那么你就可以开始根据对硬件的特定假设来推断这两种方法之间的性能差异。

要问的问题: 我需要执行多少计算步骤,有多少独立的资源系统来执行它们?是否有计算步骤的子集需要使用独立的系统子组件,并且可以从并发使用中受益?我有多少处理器核心,使用多个处理器或线程在不同的核心上完成任务的开销是多少?

如果您的任务在很大程度上依赖于独立的子系统,那么异步解决方案可能是不错的选择。如果需要处理它的线程数量很大,这样上下文切换对于操作系统来说就变得非常重要,那么单线程异步解决方案可能更好。

每当任务被同一资源绑定(例如多个需要并发访问同一网络或本地资源) ,那么多线程可能会引入不令人满意的开销,而单线程异步 引入较少的开销,在这种资源有限的情况下,它也不能产生加速。在这种情况下,唯一的选择(如果你想提高速度)是使该资源的多个副本可用(例如,如果稀缺资源是 CPU,多个处理器核心; 如果稀缺资源是连接受限的数据库,一个更好的数据库支持更多并发连接,等等)。

另一种说法是: 允许操作系统交错使用两个任务的单个资源,比仅仅让一个任务使用资源而另一个任务等待要快,然后让第二个任务顺序完成。此外,交织的调度器成本意味着在任何实际情况下它实际上都会产生一个减速。交错使用 CPU、网络资源、内存资源、外围设备或任何其他系统资源并不重要。

在 Node 中,启动了多个线程,但它是 C + + 运行时的下一层。

“所以 Yes NodeJS 是单线程的,但这是半真半假的,实际上它是事件驱动的,并且是带有后台工作者的单线程的。主事件循环是单线程的,但大多数 I/O 工作在不同的线程上运行,因为 Node.js 中的 I/O API 设计上是异步/非阻塞的,以适应事件循环。”

Https://codeburst.io/how-node-js-single-thread-mechanism-work-understanding-event-loop-in-nodejs-230f7440b0ea

”Node.js 是非阻塞的,这意味着所有函数(回调)都被委托给事件循环,并且它们由(或者可以由)不同的线程执行。由 Node.js 运行时处理。”

Https://itnext.io/multi-threading-and-multi-process-in-node-js-ffa5bb5cde98

“ Node 更快,因为它是非阻塞的... ...”的解释有点市场营销的味道,这是一个很好的问题。它高效且可伸缩,但并不完全是单线程的。

让我给出一个异步 I/O 不工作的反例。 我正在编写一个类似于以下的代理使用加强: : asio。 Https://github.com/arashpartow/proxy/blob/master/tcpproxy_server.cpp

然而,我的情况是,传入(从客户端)消息是快的,而传出(到服务器端)是慢的,为了跟上传入速度或最大化总代理吞吐量,我们必须在一个连接下使用多个会话。

因此,这个异步 I/O 框架不再工作了。通过为每个线程分配一个会话,我们确实需要一个线程池来发送到服务器。