非阻塞式编程风格有哪些优点?

4
我正在尝试理解非阻塞编程的核心原则(以及像Project Reactor这样的框架)。其主要思想是拥有一个“线程池”,其中包含确定数量的线程(执行者)和在其中执行的任务。我们不应该有任何被阻塞的线程。在“用户代码”中,我们只需运行某些内容以执行并留下回调(对结果进行操作)。我们的“用户”线程没有被阻塞,对吧?但是如果我的任务依赖于某个jdbc查询呢?我的任务将请求此查询,然后将被阻塞等待结果,对吧?因此,这个线程被阻塞了。
但是我们避免了线程创建(这是昂贵的)。这是这种风格的核心优势吗?
如果我的线程池由2个执行者组成,并且两者都被阻塞等待某些内容,其他任务将不会被执行,对吧?如何避免这种情况?创建超过2个线程吗?

3
不要使用JDBC,因为它是阻塞式的API。这就是为什么Spring正在开发R2DBC,一种非阻塞式API,使用反应式、非阻塞式数据库驱动程序。目标不是提高性能,而是提高可扩展性:能够处理大量并发请求,而无需每个请求都需要专用线程。 - JB Nizet
好的,让我们转向Reactive Mongo Driver。它是非阻塞的。但是当我查询某些内容时,它必须在某个地方等待结果,对吧?我的查询是一组TCP数据包,而且某些线程必须等待响应的TCP数据包,对吧?而且在等待期间,它无论如何都会被阻塞。如果我错了,请纠正我 :) 这是理解这种风格的关键。 - Вадим Парафенюк
非阻塞IO(或异步IO)告诉相关的驱动程序(内核、DB驱动程序等)初始化IO操作,然后线程继续做其他事情。根据您使用的技术,您可以在回调中处理异步IO结果(可能是异常),如Node.js中的回调、Java中的通道、C++中的futures、新版本的Node.js中的promises、.Net中的Tasks、C++17中的协程等。 - Sofo Gial
1
非常感谢您提供的非常有用的链接 :) “异步IO不使用线程使IO异步。这是关键点。”这正是我需要知道的 :) - Вадим Парафенюк
2个回答

16

线程是相对昂贵的系统资源。例如,每个线程都需要为调用堆栈分配内存。这取决于操作系统,但通常是1或2 MB左右。这意味着启动数千个线程不是一个好主意——仅在1000个线程的调用堆栈上就会浪费1或2 GB的内存。

因此,为了更有效地处理事务,您希望限制线程的数量,例如使用线程池来处理工作。线程池使得管理正在使用的线程数成为可能。

然而,想象一下,您有一个有10个线程的线程池,然后有10个请求进来了。你的每个线程都将被保留来处理一个请求。当它们正在忙碌时,你无法处理第11个请求,因为没有空闲线程。如果你使用阻塞I/O,那么即使你的所有10个线程都没有做任何事情(等待I/O完成),请求#11也不能被处理...

当您使用非阻塞I/O时,线程永远不需要等待I/O——因此当处理请求#3被暂停因为需要I/O操作的结果时,正在处理它的线程可以暂时切换到处理其他请求。

因此,使用非阻塞I/O,您永远不会有等待的线程,并且更有效地使用系统资源。

这只有在从系统前端到后端都使用非阻塞I/O时才有效。如果在后端使用JDBC(一种阻塞API),那么你将失去非阻塞I/O的全部好处。

因此,如果您在后端拥有数据库,则最好使用支持非阻塞I/O的数据库。一些NoSQL数据库(如MongoDB)支持此功能,对于某些关系型数据库,有特殊的驱动程序/ API可用于支持此功能。在这种情况下,您将不会使用JDBC,因为JDBC是一个固有的阻塞API。
Oracle正在开发一种名为ADBA的新API,用于关系型数据库,它将允许您使用非阻塞/异步I/O与关系型数据库进行交互,但该API尚未准备好。

感谢您提供如此丰富的解释。当我们收到请求并将其发送到我的非阻塞控制器(例如返回Mono<ResponseObject>)时,它会在某个线程池中(例如reactor http线程池)执行。因此,当返回Mono时,接收任务已经完成,并且稍后将创建新任务以响应此请求(当mongodb返回所需对象时),对吗? - Вадим Парафенюк
这是一个很好的例子,展示了你的应用程序如何使用线程 https://kamilszymanski.github.io/resources-utilization-in-reactive-services/ - kojot
@ВадимПарафенюк 当你使用例如Spring WebFlux进行响应式编程时,响应式库将为您处理线程,并且实际工作并非在调用控制器方法的那一刻完成,而是在实际数据由数据库返回的那一刻完成。 - Jesper
@Jesper,所以在这种情况下,Web框架(Spring WebFlux)首先从所需的控制器获取我的发布者(它将从数据库+管道发布数据),并将其订阅到一些神奇的订阅者中,该订阅者可以收集元素(如果是Flux),然后在完成后将其作为响应发送。这是WebFlux工作流程的粗略描述吗? - Вадим Парафенюк
@ВадимПарафенюк 是的,大致上就是这样工作的;框架从你的控制器(一个 Mono 或 Flux)获取发布者,然后订阅它以完成实际工作,以使没有线程需要阻塞。 - Jesper

2

Project Reactor是Reactive Streams规范的一种实现。规范概述可在ReactiveManifest找到。它不仅仅是创建一组线程并让它们完成工作,而是通过框架或运行时(在此示例中为Project Reactor)以这样的方式组织代码,使其表现为非阻塞状态。此外,整个系统实现必须按照这种方式进行,否则您将无法从响应式流中受益。

如果我的线程池包含2个执行器,并且两者都被阻止等待某些内容,则其他任务不会被执行,对吗?如何避免这种情况?创建超过2个线程?

答案可能是肯定的,也可能是否定的。框架可以或不可以创建线程。由于代码将在线程之间交错执行,因此在非阻塞系统中,包括低级操作(例如libuv I/O),不需要线程等待I/O操作的完成。同时,线程将执行一些有意义的事情。完成任务后将通知相关代码,可以由任何可用线程执行依赖代码。这种系统的目标是利用有限资源(线程)充分利用CPU。

选自http://www.reactive-streams.org响应式流的主要目标是在异步边界上管理流数据的交换 - 想象将元素传递到另一个线程或线程池 - 同时确保接收方不被迫缓冲任意数量的数据。换句话说,在此模型中,反压力是其内在部分,以便允许在线程之间介导的队列受到限制。如果反压缩的通信是同步的(请参见响应式宣言),则异步处理的好处将被否定,因此必须注意强制所有Reactive Streams实现的完全非阻塞和异步行为。

React框架强制执行并帮助您从基础开始构建完全非阻塞的系统。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接