Node.js 如何解决I/O操作阻塞的问题
Node.js使用非阻塞的I/O操作机制,这使得你可以在程序运行时只有一个线程执行。如果Node.js必须使用阻塞的I/O操作,那么在等待I/O操作完成时,你将无法做任何其他事情。下面是一个示例图像,展示了Node.js需要使用阻塞I/O操作时的情况:
Node.js项目包括JavaScript引擎、事件循环和I/O层。它通常被称为非阻塞Web服务器。
注意: “I/O”通常是指系统磁盘和网络之间的交互,由libuv支持。
为什么阻塞I/O操作是一个问题?
在传统的编程语言(如C和PHP)中,默认情况下,所有指令都被阻塞,除非你明确地“启用”它执行异步操作。假设你发起了一个网络请求来读取一些数据,或者你想要读取一些文件并将其数据显示给用户,但是这个线程的执行被阻塞,直到响应准备好。为了解决这个阻塞I/O流的问题,JavaScript允许你以非常简单的方式创建异步和非阻塞代码,使用单线程、回调函数和事件驱动的编程方式。
让我们通过一个示例来更好地理解这个!
示例1: 以下示例使用readFileSync()函数读取文件并演示Node.js中的阻塞:
输出:
解释: 在上面的示例中,我们可以看到阻塞方法是同步执行的[或者你可以说是逐行执行]。现在你看到每一行代码都等待前一行执行完毕才能得到结果,这可能会成为一个问题,特别是对于读取、更新或者其他与I/O相关的慢操作,因为每一行代码都会阻塞后续代码的执行,我们称之为阻塞代码,因为下一行代码只有在前一行执行完毕后才能执行,由于Node.js的设计方式,这变成了一个庞大的问题,这导致我们使用非阻塞方法来解决,即代码的执行是异步的,意味着不是逐行执行,而是使用回调函数来实现这种非阻塞行为。下面是同样的示例,我们使用了上面描述同步行为的回调函数进行了修改,使之变成了异步的。
示例2: 下面的示例使用readFile()函数来读取文件并展示Node.js中的非阻塞行为。
输出:
注意: 在上面的示例中,我们看到在非阻塞方法中,控制台实际上在文件内容之前打印了消息。这是因为程序不等待readFile()函数返回并转到下一个操作,使其异步。当readFile()函数返回时,它打印文本文件的内容。
Node.js如何解决这个问题?
你可能听说过非阻塞I/O的概念,以及Node.js如何使用它来解决阻塞调用问题并实现超快速运行,但是非阻塞I/O是什么,为什么它有用?我们稍后会理解这个问题,但首先,您需要了解服务器和线程是如何工作的,以及服务器如何处理请求。在转向Node.js之前,让我们简要介绍一下服务器和线程。
服务器实际上就是接受请求并执行一些工作来计算需要发送的响应的东西。例如,当您访问google.com时,您会向Google服务器发送一个HTTP请求,该服务器计算您在浏览器主页上应该看到的HTML响应。但在内部,服务器将工作分配给一个或多个线程(一个线程可以被视为一个单一的工作人员),同时处理其他用户的需求和请求,而不阻塞主线程。
我们可以通过使用餐厅作为类比来探索这一点,在一个经常被访问的餐厅中,只有一个服务员,因为它并不是很受欢迎,客户往往一个接一个地来,服务员在转向下一个顾客之前为每一家服务,当服务员完成一桌客人后,要么等待更多客户,要么转到另一个客户。这个示例很好地描述了服务器的工作方式。
我们刚刚提到的餐厅就像一个服务器,它频繁地接收请求,而服务员就像一个线程,请求就像顾客。
现在想象一下,如果两方同时进入餐厅,而一个服务员一次只能为一个人提供服务,但实际上不是这样的。你看,当顾客忙于查看菜单时,他们不需要你的帮助,所以服务员可以移动或者可以说在桌子之间切换,同时帮助两方,因为每个人都需要帮助,而不需要让某人站立或等待他们的轮到。
如果两个朋友同时需要帮助,那么一个人需要稍等一段时间,比起只有一方需要帮助时等待的时间会更长一些。基本的理念是,一个服务员可以同时为多张桌子提供服务,因为每张桌子都有一些空闲时间。
所有这些原理也适用于服务器和Node.js!
就像桌子一样,我们需要帮助来处理时间请求,也有一部分需要主动注意的部分,而不需要主动关注的部分通常称为CPU工作,因为它需要计算机的中央处理单元进行思考和计算结果。
CPU工作需要一个线程来处理,就像一个桌子需要一个服务员来处理一样,但是不需要主动关注的部分称为I/O,因为它在等待其他东西提供输入或发送输出。
这里是结束部分,就像服务员可以通过换桌子来节省时间一样,同样的阻塞可以通过改变请求来浪费线程的时间,当一个请求进行I/O操作时,它极大地增加了I/O可以完成的有效工作量!
注意: 所以,解决node.js中的这个问题的方法是使用异步非阻塞代码,而Node.js使用事件循环来实现这一点。 “处理和处理外部事件并将它们转化为回调调用的对象”就是事件循环。
当需要数据时,Node.js会记录回调并将操作发送到事件循环中。当有数据可用时,调用回调函数。所以基本上,我们将繁重的工作转移到后台运行,然后当任务完成时,调用回调函数来处理之前保存的结果,在此期间,其余的代码仍然可执行,而当前阻塞的繁重任务在后台运行。
简而言之:
除了你的代码之外,一切并行运行,我们总是尝试传递一个回调函数,一旦我们完成任务并继续处理,就会调用它。在继续执行其余代码之前,我们不会等待此操作结束。