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


117

我在网上搜索了有关阻止I / O和非阻止I / O的一些技术细节,我发现一些人指出,非阻止I / O比阻止I / O更快。例如在本文档中

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

除了减少CPU空闲时间以外,我还看到了另一个选择,可以增加计算机在给定时间范围内可以执行的任务数量:减少切换线程带来的开销。但是如何做到这一点?开销是否足够大以显示可衡量的效果?这是关于如何描述其工作原理的想法:

  1. 要加载文件的内容,应用程序将此任务委托给基于事件的I / O框架,并传递回调函数和文件名。
  2. 事件框架委托给操作系统,该操作系统对硬盘的DMA控制器进行编程,以将文件直接写入内存
  3. 事件框架允许进一步的代码运行。
  4. 在完成磁盘到内存的复制后,DMA控制器将引起中断。
  5. 操作系统的中断处理程序将基于事件的I / O框架通知文件已完全加载到内存中。它是如何做到的?使用信号??
  6. 事件I / O框架中当前运行的代码完成。
  7. 基于事件的I / O框架检查其队列,并查看步骤5中的操作系统消息,并执行在步骤1中获得的回调。

那是怎么回事?如果没有,它如何工作?这意味着事件系统可以工作而无需显式地触摸堆栈(例如,真正的调度程序需要在切换线程时备份堆栈并将另一个线程的堆栈复制到内存中)?这实际上节省了多少时间?还有更多吗?


5
简短的答案:更多的是每个连接都有一个线程的开销。非阻塞io让每个连接避免使用线程。
Dan D.

10
在无法创建与连接数一样多的线程的系统上,阻塞IO的成本很高。在JVM上,您可以创建数千个线程,但是如果连接数超过100.000,该怎么办?因此,您必须坚持使用异步解决方案。但是,在Go / Erlang / Rust中,有些语言的线程并不昂贵(例如,绿色线程),拥有100.000个线程也不成问题。当线程数量很大时,我相信阻塞IO会产生更快的响应时间。但这是我还必须向专家询问的事实是否如此。
OlliP 2014年

@OliverPlow,我也这样认为,因为阻塞IO通常意味着我们让系统处理“并行管理”,而不是使用任务队列等自己完成。
Pacerier 2014年

1
@DanD。,如果拥有线程的开销等于执行非阻塞IO的开销怎么办?(通常在绿色线程的情况下适用)
Pacerier 2014年

不会发生“复制堆栈”。不同的线程具有不同地址的堆栈。每个线程都有其自己的堆栈指针以及其他寄存器。上下文切换仅保存/恢复架构状态(包括所有寄存器),而不保存/恢复内存。在同一进程的线程之间,内核甚至不必更改页表。
彼得·科德斯

Answers:


44

非阻塞或异步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(WriteFile具有OVERLAPPED结构)的完成。回调函数才会被调用,例如当你的代码的调用WaitForMultipleObjectsExbAlertable设置为true

在网上阅读更多内容:


从网络的角度来看,常识(互联网,专家的评论)建议最大程度地提高 由于内存增加和上下文切换时间的原因,请求线程数在阻止IO(使请求的处理甚至变得更慢)方面是一件坏事,但是,当将作业推迟到另一个线程时,异步IO不会做同样的事情吗?是的,您现在可以处理更多请求,但是后台具有相同数量的线程。.这样做的真正好处是什么?
JavierJ 2015年

1
@JavierJ您似乎相信,如果n个线程执行异步文件IO,将另外创建n个线程来执行阻止文件IO?这不是真的。该操作系统具有异步文件IO支持,并且在等待IO完成时不需要阻塞。它可以将IO请求排队,并且如果发生硬件(例如DMA)中断,则可以将请求标记为已完成,并设置一个向调用方线程发送信号的事件。即使需要额外的线程,操作系统也可以将该线程用于来自多个线程的多个IO请求。
Werner Henze 2015年

谢谢,这涉及到OS异步文件IO支持是有意义的,但是当我为该实现的实际实现编写代码时(从Web的角度来看)说,使用Java Servlet 3.0 NIO,我仍然看到请求线程和后台线程(异步)循环读取文件,数据库或其他内容。
JavierJ

1
@piyushGoyal我重新回答了。我希望现在更加清楚。
Werner Henze

1
在Windows上,使用异步文件I / O意味着写操作的大小必须是页面大小的倍数。-不,不是。您正在考虑无缓冲的I / O。(它们经常一起使用,但不一定必须使用。)
哈里·约翰斯顿

29

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操作阻止了请求,则存在耗尽Web池并使Web应用程序冻结或响应缓慢的风险。

我注意到的一件事是,异步I / O在处理非常快速的I / O操作时不是最佳选择。在那种情况下,在等待I / O操作完成时不让线程繁忙的好处不是很重要,并且该操作在一个线程上开始,而在另一个线程上完成则增加了整个执行的开销。

您可以在此处阅读我最近对异步I / O与多线程进行的详细研究。


我想知道是否有必要在预期的I / O操作与可能不会发生的事情之间进行区分(例如,在远程设备可能会或可能不会的情况下,例如“获取到达串行端口的下一个字符”)发送任何内容]。如果I / O操作预期在合理的时间内完成,则可能会延迟清除相关资源,直到操作完成。但是,如果操作可能永远不会完成,则这种延迟将是不合理的。
supercat

@supercat您描述的场景用于较低级别的应用程序和库。服务器一直依赖它,因为它们不断等待传入的连接。如上所述的异步I / O在这种情况下不适合,因为它基于启动特定操作并注册回调以完成操作。在您描述的情况下,您需要在系统事件上注册回调并处理每个通知。您正在不断处理输入,而不是执行操作。如前所述,这通常是在较低级别完成的,几乎不会在您的应用程序中完成。
Florin Dumitrescu

这种模式在各种类型的硬件附带的应用程序中很常见。串行端口不像以前那样普遍,但是模拟串行端口的USB芯片在专用硬件的设计中非常流行。此类字符将在应用程序级别进行处理,因为操作系统将无法得知输入字符序列意味着打开了一个现金抽屉,并且应将通知发送到某个地方。
supercat

我不认为有关阻塞IO的CPU成本的部分是准确的:处于阻塞状态时,触发阻塞IO的线程将由操作系统等待,并且在IO完全完成之前不会花费CPU时间,只有在此之后OS(通过中断通知)恢复被阻塞的线程。您所描述的(长时间轮询的繁忙等待)不是在几乎所有运行时/编译器中如何实现阻塞IO的方法。
Lifu Huang

4

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


4

要假定由于任何形式的多重计算而导致的速度提高,您必须假定正在多个计算资源(通常是处理器内核)上同时执行多个基于CPU的任务,或者不是所有任务都依赖于并发使用相同的资源-也就是说,某些任务可能依赖于一个系统子组件(例如磁盘存储),而某些任务依赖于另一个(接收来自外围设备的通信),而其他任务可能需要使用处理器内核。

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

用隐性假设来谈论所有这些很容易。例如,有些人很快就宣称“异步I / O将比多线程I / O更快”。这种说法令人怀疑,原因有几个。首先,可能是某些给定的异步I / O框架是通过多线程精确实现的,在这种情况下,它们是相同的,并且说一个概念“比另一个概念更快”是没有意义的。 。

其次,即使在异步框架的单线程实现(例如单线程事件循环)的情况下,您仍然必须假设该循环在做什么。例如,单线程事件循环可以做的一件愚蠢的事情是要求它异步完成两个不同的,纯粹受CPU约束的任务。如果您是在一台只有理想化的单处理器内核(忽略现代硬件优化)的机器上执行此操作,那么“异步”执行此任务与使用两个独立管理的线程或仅使用一个单独的进程执行该操作实际上并没有什么不同- -差异可能归结于线程上下文切换或操作系统调度优化,但如果两个任务都交给CPU,则在两种情况下都是相似的。

想象一下您可能遇到的许多异常或愚蠢的极端情况,这很有用。

“异步”不必是并发的,例如如上所述的:您可以“异步”在具有一个处理器核心的计算机上执行两个CPU绑定的任务。

多线程执行不必是并发的:您在具有单个处理器核心的计算机上产生两个线程,或者要求两个线程获取任何其他种类的稀缺资源(例如,一个网络数据库只能建立一个一次连接)。线程的执行可能是交错的,但是操作系统调度程序认为合适,但是它们的总运行时间无法减少(并且会因线程上下文切换而增加)(在更一般的情况下,如果产生的线程多于线程数)核心来运行它们,或者有更多线程在请求​​资源,而不是资源可以承受的资源。同样的事情也适用于多处理。

因此,异步I / O和多线程都不必在运行时间方面提供任何性能提升。他们甚至可以放慢速度。

但是,如果您定义了一个特定的用例,例如一个特定的程序,它既可以进行网络调用来从与网络连接的资源(例如远程数据库)中检索数据,又可以进行一些本地CPU绑定的计算,那么您就可以开始推理给定关于硬件的特定假设,这两种方法之间的性能差异。

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

如果您的任务很大程度上依赖于独立的子系统,那么异步解决方案可能会很好。如果处理该线程所需的线程数量很大,以至于上下文切换对于操作系统来说变得不那么重要,那么单线程异步解决方案可能会更好。

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

换一种说法是:允许操作系统对两个任务的单个资源的使用进行交织,不会比仅让一个任务在另一个任务等待时使用该资源,然后让第二个任务串行完成更快。此外,交错的调度器成本意味着在任何实际情况下它实际上都会导致速度降低。发生CPU,网络资源,内存资源,外围设备或任何其他系统资源的交错使用都没有关系。


2

非阻塞I / O的一种可能的实现方式就是您所说的,它具有一组后台线程,它们确实阻塞了I / O,并通过某种回调机制将I / O的发起者线程通知了该线程。实际上,这就是glibc中的AIO模块的工作方式。是有关实现的一些模糊细节。

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


2

我目前正在使用协议线程在嵌入式平台上实现异步io的过程。非阻塞io使运行在16000fps和160fps之间有所不同。非阻塞io的最大好处是,您可以在硬件完成任务的同时构造代码以执行其他任务。甚至设备的初始化也可以并行完成。

马丁


1

在Node中,正在启动多个线程,但这是C ++运行时的底层。

“所以,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更快,因为它不受阻碍...”的解释有点行销,这是一个很大的问题。它是高效且可伸缩的,但并非完全是单线程的。


0

据我所知,改进之处在于异步I / O使用了所谓的I / O完成端口(我正在谈论MS System,只是为了阐明这一点)。通过使用异步调用,框架可以自动利用这种架构,并且这比标准线程机制要高效得多。作为个人经验,我可以说,如果您更喜欢AsyncCalls而不是阻塞线程,那么您会明智地感觉到您的应用程序更具响应性。


0

让我给您一个反例,异步I / O不起作用。我正在编写类似于下面使用的boost :: asio的代理。 https://github.com/ArashPartow/proxy/blob/master/tcpproxy_server.cpp

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.