乐趣区

Nodejs之异步编程

文章原创于公众号:程序猿周先森。本平台不定时更新,喜欢我的文章,欢迎关注我的微信公众号。

其实对大部分的开发者来说,异步编程与一般自然语言的线性思维会有所冲突。所以大部分开发者不能适应直接面向事件驱动进行编程,Node.js 是首个将异步编程带到应用层面的平台,Node.js 无时无刻不透露出异步的信息。在接触 Node 的过程中,很多人只是很粗略的接触了几个回调函数之后就放弃了,确实 Node 使用异步编程,容易陷入回调地狱,但是 Node 异步编程的难题其实已经基本解决,可以通过事件发布 / 订阅模式,或者通过 Promise/Defferred 模式,其实都可以完美的去解决回调陷阱的问题。

其实大部分开发人员都习惯线性思维去思考问题,所以同步编程一直很流行。但是单线程同步模型中,CPU 与 I / O 操作无法重叠进行,所以性能问题也就摆在了开发人员的面前。在大多数语言中,提高性能的方式一般使用多线程的方式解决,但是多线程中线程切换耗费的开销,以及锁以及线程同步等问题,所以多线程会给开发人员业务逻辑带来麻烦。而 Node 直接采用异步编程,可以使 CPU 与 I / O 操作并行不用相互等待,可以让资源等到更好的利用。

异步 IO 与非阻塞 IO 的区别

非阻塞 IO 是由于完整的 I / O 没有完成,立即返回的并不是我们执行的最终数据,而仅仅是返回当前的调用状态,为了获得完整数据需要进行轮询重复调用 I / O 操作确认是否完成。异步 I / O 可实现不等待数据读取完成,执行 I / O 操作后后立刻返回,数据写入缓存,由底层完成监听操作,并返回成功或失败的信息给应用。

异步编程优点

Node.js 最大的优点莫过于基于事件驱动的非阻塞 I / O 模型,非阻塞 I / O 可以使 CPU 与 I / O 操作不用相互等待,可以让资源可以得到更好的利用。Node.js 为了解决编程模型中阻塞 I / O 的性能问题,采用单线程异步模型,所以 Node.js 更适合 I / O 密集型问题,因为 Node.js 面向驱动进行编程,所以需要面对海量请求,当海量请求同时作用在单线程上时,就需要防止任何一个会过度消耗时间片的请求。所以只要合理利用 Node.js 的异步模型,加上 V8 引擎的高性能,就可以充分发挥 CPU 与 I / O 并行的优势。

异步编程的难点

Node.js 借助异步 I / O 模型以及 V8 引擎,突破了单线程的性能瓶颈,让 JavaScript 在后端体现了实用价值。但是也由于异步编程会给开发者带来一些难点。

(1) 函数嵌套过深

在前端 JavScript 中,DOM 事件绑定一般较少存在多重事件绑定的情况。一般为不同的 DOM 元素绑定不同的事件。

但是对于 Node.js 而言,多个异步调用的场景比比皆是。

其实对于最后的结果来说这样的函数嵌套结构是没有任何问题的,但是这样并没有利用好 Node.js 异步 I / O 带来的并行优势。而且函数嵌套过深,对于开发人员的后期维护也会造成困难。

(2) 阻塞代码

在 JavaScript 中,并没有类似 Java 的 sleep() 这样的线程沉睡功能,可以进行延时操作的只有 setInterval() 和 setTimeout() 这两个函数。那么如果我们需要在 JavaScript 中实现延迟 1s 要怎么做呢?其实大部分开发者可能会这么去进行实现:

但是请记住 Node.js 是单线程模型,所以在执行的时候 CPU 资源会全部为了这段代码进行服务。从而导致其他请求全部被视而不见。所以我们可以采用 setTimeout 改写一下代码效果会更好:

但是这里有一个问题,如果我把后面的时间设成 0,是不是意味着马上执行代码呢?这个问题大家可以思考一下,对答案感兴趣的可以直接在公众号发消息,我会及时回复。

(3) 多线程编程

因为 Node.js 是单线程模型,对于多核 CPU 服务端而言,其实 Node.js 单进程是没有充分利用好多核 CPU 的,所以浏览器可以将 JavaScript 与 UI 渲染分离,就可以更好的去利用多核 CPU 为大量计算做服务。但是这种开发模式开发者要面临跨线程的编程,对于 JavaScript 一直走的单线程编程路线来说会增加一定的难度。

(4) 异常处理

我们在使用 Java 进行异常处理其实是非常方便的,可以直接通过 try/catch/finally 语句进行异常捕获以及异常处理。

但是在异步编程中这种常用的异常处理并不一定适用,因为前面有讲过异步编程时异步 I / O 提交请求后马上返回,因为异常一般不会发生在此阶段,这时候你对这段代码执行 try/catch 操作进行异常捕获其实不会发挥作用,因为 try/catch 只能捕获当次事件内发生的异常,对事件执行结束返回的回调函数 callback 中抛出的异常其实是无能为力的,所以在 Node.js 中,将异常作为回调函数 callback 的第一个参数传回,如果为空时,则表示回调函数没有抛出任何异常。

上述代码中,执行 checkLogin 如果出现异常,则回调函数的第一个参数 err 则不为空,我们就可以根据这个 err 参数对异常进行处理。

异步编程解决方案

  1. 事件发布 / 订阅模式
  2. Promise/Deferred 模式
  3. 流程控制库

由于这三种方案涉及知识点较杂,这篇文章暂时不对这三种方案作具体介绍,下一篇文章会对这三种方案作具体介绍。

异步并发控制

在 Node.js 中,我们可以很轻易的利用异步发起并行调用,但是如果并发量过大,我们的服务器会承受不住,比如如果是对文件系统进行大量并发调用,操作系统的文件描述符数量会在瞬间被用光。所以对于异步编程来说并发很容易实现,但是也要有一定的过载保护。这里主要讲一种过载解决方案:async.

async 提供了一个方法 parallelLimit() 用于处理异步调用的限制。

parallelLimit() 方法有一个用于限制并发数量的参数,使得任务只能同时并发一定数量,而不能无限量同时并发。上面的代码,我们并发数设置为 1,所以只能同时并发一个任务。

但是 parallelLimit() 有一个缺点:无法动态的添加并行任务。但是 async 提供了 queue() 方法可以动态添加并行任务,这对于遍历文件目录等操作是非常高效的。但是 queue() 接收的参数是固定的,丢失了 parallelLimit() 的多样性。

本篇到这里内容就结束了,本篇主要还是偏概念,可能得对 Node.js 有一定了解才更适合,所以这里推荐 csdn 一篇比较基础的关于异步编程的文章:https://blog.csdn.net/O4dC8Oj…

欢迎关注我个人公众号:程序猿周先森

退出移动版