Skip to content

理解Node中的IO

✍️ w 🕒 2024-05-19 08:09:58(a month ago) 🔗 B.NodeJS学习 node

Node.js 是一个基于事件驱动、非阻塞I/O模型的JavaScript运行时,因此在 Node.js 中,I/O 操作是非常重要的一部分。以下是 Node.js 中常用的 I/O 操作

  • 文件 I/O:Node.js 提供了一组文件系统(fs)API,可以对文件进行读写操作,例如:读取文件、写入文件、创建目录和删除文件等。

  • 网络 I/O:Node.js 提供了一组网络 API,可以创建和管理网络连接,例如:创建 HTTP/HTTPS 服务器、创建 TCP/UDP 客户端等,还可以使用 Socket.IO 等库进行 WebSocket 编程。

  • 控制台 I/O:Node.js 提供了控制台输出 API,例如:console.log()、console.error() 等,可以在控制台输出日志信息。

  • 进程 I/O:Node.js 提供了一组进程 API,可以对进程进行管理,例如:创建子进程、发送信号等。

  • 事件 I/O:Node.js 是基于事件驱动的,因此事件 I/O 是非常重要的一部分。Node.js 提供了一组事件相关的 API,例如:EventEmitter、process.nextTick() 等。

进程和线程

说到 Node 时候经常说到,Node.js的I/O模型属于事件驱动和异步I/O模型。这种设计使得 Node.js 可以通过事件驱动和异步 I/O 处理大量并发连接或文件访问。因此非阻塞 I/O 的特性,使得它在处理高并发场景下具有很好的性能。

那为什么 Node 可以做到,先了解 进程 和 线程进程(Process)和线程(Thread)是操作系统中的基本概念,用于实现并发执行和多任务处理。它们是计算机程序运行时的执行单元,但在功能和特性上有一些区别。

线程是操作系统能够进行运算调度的最小单位。他被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每个线程并行执行不同的任务。

当你打开应用程序时,操作系统会为每个应用程序创建一个独立的进程。每个进程都有自己的内存空间和资源,它们相互独立,互不干涉。进程划分了程序要使用的内存。

在进程内部,可以创建多个线程来执行任务。线程是进程内部的执行单元,多个线程共享进程的资源和内存空间。线程与CPU进行联系和调度,如果一个线程崩溃,可能会导致整个进程的异常终止。

进程之间相互隔离,一个进程的崩溃不会影响其他进程的运行。但是,进程内部的线程共享相同的资源,一个线程的崩溃可能会对整个进程产生影响。

总结起来,进程是程序的实例,进程之间相互独立,进程划分了程序要使用的内存。线程是进程内部的执行单元,多个线程共享进程的资源和内存空间,一个线程的崩溃可能会影响整个进程的运行。

进程:要以一个整体的形式暴露给操作系统管理,里面包含对各种资源的调用,堆存的管理,网络接口调用等,对资源管理的集合就可以成为进程

  • 独立性:每个进程都有自己的地址空间和资源,相互之间不会干扰。
  • 资源开销:创建一个新进程需要分配内存和其他系统资源,因此进程间切换的开销较大(因为給开辟内存空间)
  • 并发执行:多个进程可以同时执行,利用多核处理器的并行性能。
  • 利用cpu:同时利用多个cpu,能够同时进行多个操作。这里特别补充 ,进程不是越多越好,cpu个数等于进程个数比较推荐举个例子假设你在家里做饭,你有两个炉子(这就是CPU)。如果你只有一个菜要做,那么你只需要一个人(这就是进程)来做就够了。如果你有两个菜要做,那么你就需要两个人,每个人负责一个炉子,这样可以同时做两个菜,提高效率。但是,如果你有三个菜要做,你还是只有两个炉子,那么你就需要三个人轮流使用炉子。这样就会比较麻烦,因为人们需要等待炉子空闲下来才能使用,这就浪费了时间。所以,进程的数量并不是越多越好,最好的情况是进程的数量等于CPU的数量,这样每个进程都可以有自己的CPU来运行,不需要等待,可以提高效率。

线程:是操作系统最小的调度单位,是一串指令的集合

  • 共享性:线程共享进程的内存和资源共享内存,可以直接访问进程的数据。io操作的时候,创作并发操作
  • 轻量级:创建和切换线程的开销较小,因为它们共享进程的资源
  • 并发执行:多个线程可以同时执行,利用多核处理器的并行性能。也不是越多越好,具体案例具体分析,请求上下文切换耗时
  • 互斥访问:某些资源只能被一个线程独占访问,例如一个文件或一个共享的数据结构。在这种情况下,需要使用互斥锁(mutex)或其他同步机制来确保同一时间只有一个线程能够访问该资源,其他线程需要等待。
  • 竞态条件:当多个线程并发地访问和修改共享的数据时,由于执行顺序的不确定性,可能会出现意外的结果。这种情况下,需要使用同步机制(如互斥锁、信号量)来保证对共享数据的访问的一致性和正确性。
  • 死锁:在多个线程之间存在循环依赖的资源请求关系时,可能会导致死锁。即每个线程都在等待其他线程释放资源,导致所有线程都无法继续执行。为避免死锁,需要设计良好的资源请求和释放策略,或者使用死锁避免算法。

cpu多核和进程线程之间的关系,需要先知道CPU的多核则是硬件层面的概念,一个多核CPU就像是多个CPU的集合,它们共享一些资源(例如缓存和内存总线),但每个核心都有自己的整套寄存器。

  • 单核CPU与进程/线程:在单核CPU系统中,虽然一次只能执行一个任务(进程或线程),但操作系统通过快速切换任务,使得用户感觉多个任务在同时运行。这就是所谓的并发。

  • 多核CPU与进程/线程:在多核CPU系统中,可以真正并行地执行多个任务(进程或线程)。例如,如果有两个核心,那么可以同时执行两个任务。这就是所谓的并行。

在多核CPU系统中,操作系统可以更有效地分配任务,因为每个核心可以独立地执行一个任务。这在处理多任务和多线程程序时特别有效。如果一个程序被设计为多线程,并且可以并行处理,那么在多核系统中,它可以比在单核系统中运行得更快。这就是并行计算的优势

Node 的IO 优势

知道了如果想并发最常见的形式,使用多进程或者多线程,但是相对的问题是会增加开销,举个例子在 Java、PHP 或者 .net 等服务器端语言中,会为每一个客户端连接创建一个新的线程。而每个线程需要耗费大约 2MB 内存。也就是说,理论上,一个 8GB 内存的服务器可以同时连接的最大用户数为 4000 个左右。要让 Web 应用程序支持更多的用户,就需要增加服务器的数量,而 Web 应用程序的硬件成本当然就上升了。

但如果只是单线可以避免这个问题,但是因为单线会阻塞实际并发后的执行时间会变长,Node 作为单线成 I/O 操作的实现方式是异步非阻塞的模型,即当发起一个I/O请求时,Node.js 并不会阻塞主线程的执行,而是将其放到一个专门的 I/O 线程中执行,然后通过事件循环机制等待 I/O 线程完成操作,将结果返回给主线程进行后续处

不是说js 是单线程么,怎么出现了一个 I/O 线程?node单线程是对我们观察而言。对底层可不是,只不过其他线程对你不开放的,如果node真的只是单纯的单线程那么就变成同步阻塞模型,这种模型下的 I/O 操作会阻塞主线程的执行,直到 I/O 操作完成并返回结果后才能继续执行后面的代码。这种模型在高并发的情况下效率低下,容易导致系统瓶颈和响应延迟。

例如,当在访问数据库取得数据的时候,需要一段时间。在传统的单线程处理机制中,在执行了访问数据库代码之后,整个线程都将暂停下来,等待数据库返回结果,才能执行后面的代码。也就是说,I/O阻塞了代码的执行,极大地降低了程序的执行效率。 由于 Node.js 中采用了非阻塞型I/O机制,因此在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率。 当某个I/O执行完毕时,将以事件的形式通知执行I/O操作的线程,线程执行这个事件的回调函数。为了处理异步I/O,线程必须有事件循环,不断的检查有没有未处理的事件,依次予以处理。 阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程的 CPU 核心利用率永远是 100%。所以,这是一种特别有哲理的解决方案:与其人多,但是好多人闲着;还不如一个人玩命,往死里干活儿。

这样从新来看同样是客户端并发 Node.js 不为每个客户连接创建一个新的线程,而仅仅使用一个线程。当有用户连接了,就触发一个内部事件,通过非阻塞I/O、事件驱动机制,让 Node.js 程序宏观上也是并行的。使用 Node.js ,一个 8GB 内存的服务器,可以同时处理超过 4 万用户的连接。 另外,单线程带来的好处,操作系统完全不再有线程创建、销毁的时间开销

Node 计算的劣势

在处理 I/O 并发上得益于 Node 异步非阻塞的模型 设计可以进行一个高并发的处理,但是善于I/O,不善于计算。因为Node.js最擅长的就是任务调度,如果你的业务有很多的 CPU 计算,实际上也相当于这个计算阻塞了这个单线程,就不太适合 Node 开发,但是也不是没有解决方案,只是说不太适合。

常见的CPU 密集任务加密、解密、压缩、解压缩、图像处理、视频编码、科学计算等,这一类的任务都会阻塞Node

在举个例子

Node.js(正如浏览器)里的 JavaScript 提供了一种单线程环境。这意味着你的程序不会有两块东西同时在运行,取而代之的是异步处理 I/O 密集操作所带来的并发。比如说 Node.js 给数据库发起一个请求去获取一些数据时,Node.js 可以集中精力在程序的其他地方:

js
// Trying to fetch an user object from the database. Node.js is free to run other parts of the code from the moment this function is invoked..
db.User.get(userId, function(err, user) {
  // .. until the moment the user object has been retrieved here
})

然而,在一个有上千个客户端连接的 Node.js 实例里,一小段 CPU 计算密集的代码会阻塞住事件循环,导致所有客户端都得等待。CPU 计算密集型代码包括了尝试排序一个巨大的数组、跑一个耗时很长的函数等等。例如:

js
 function sortUsersByAge(users) {
  users.sort(function(a, b) {
    return a.age & lt
    b.age ? -1 : 1
  })
}

在一个小的“users” 数组上调用“sortUsersByAge” 方法是没有任何问题的,但如果是在一个大数组上,它会对整体性能造成巨大的影响。如果这种事情不得不做,而且你能确保事件循环上没有其他事件在等待(比如这只是一个 Node.js 命令行工具,而且它不在乎所有事情都是同步工作的)的话,那这没有问题。但是,在一个 Node.js 服务器试图给上千用户同时提供服务的情况下,它就会引发问题。 如果这个 users 数组是从数据库获取的,那么理想的解决方案是从数据库里拿出已排好序的数据。如果事件循环被一个计算金融交易数据历史总和的循环所阻塞,这个计算循环应该被推到事件循环外的队列中执行以免占用事件循环。

总结

在 Node.js 中,采用了异步 I/O 模型来处理并发请求,通过事件循环机制和非阻塞 I/O 操作来实现异步处理请求。Node.js 的异步 I/O 模型可以处理大量的并发连接,避免了线程切换和上下文切换的开销,因此适合处理需要大量 I/O 操作的任务。

结合优势来看分析出来当应用程序需要处理大量并发的I/O,而在向客户端发出响应之前,应用程序内部并不需要进行非常复杂的处理的时候,Node.js非常适合。Node.js也非常适合与websocket配合,开发长连接的实时交互应用程序。

具体场景可以表现为如下:

  • 第一大类:用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的 web 应用程序;
  • 第二大类:基于 web、canvas 等多人联网游戏;
  • 第三大类:基于 web 的多人实时聊天客户端、聊天室、图文直播;
  • 第四大类:单页面浏览器应用程序;
  • 第五大类:操作数据库、为前端和移动端提供基于json的 API;
  • 第六大类,...

在实际的业务场景中,如果应用需要处理大量的 CPU 密集型任务,Java 的多线程模型可能更适合。而如果应用需要处理大量的 I/O 密集型任务,Node.js 的异步 I/O 模型可能更适合。当然,对于一些既有 CPU 密集型任务又有 I/O 密集型任务的应用,可以考虑使用 Java 和 Node.js 结合的方案,例如使用 Java 处理 CPU 密集型任务,使用 Node.js 处理 I/O 密集型任务。

CPU 密集型任务需要大量的 CPU 计算资源,例如加密、解密、压缩、解压缩等操作,这些操作需要大量的计算,而且计算量很大。这些任务通常需要使用多线程或者分布式计算来完成。在 Java 中,可以使用多线程来处理这些任务,而在 Node.js 中,这些任务可能会阻塞事件循环,因此需要将其放到一个单独的线程中运行。

I/O 密集型任务需要大量的 I/O 操作,例如网络请求、数据库读写、文件读写等操作,这些操作通常需要等待 I/O 操作完成才能继续执行。这些任务通常使用异步 I/O 模型来处理,可以避免线程切换和上下文切换的开销,提高系统的并发性能。在 Java 中,可以使用 NIO(New I/O)来处理这些任务,而在 Node.js 中,异步 I/O 模型是天然支持这些任务的。

以 js 和 Java 做一个场景对比。Java 虚拟机提供了更多的系统资源管理功能,适合构建大规模的企业级应用,可以通过多线程处理高并发请求。而 Node.js 适合构建轻量级的、高性能的应用,可以通过异步 I/O 模型处理大量的并发连接。在实际应用中,选择何种技术取决于具体的业务场景和需求。

参考

程序员成长指北Node

朴灵老师的《深入浅出 Node.js》

Released under the MIT License.