APP下载

基于Node.js的JavaScript并发控制流框架

2015-11-19李轶

关键词:缓冲区计数器调用

李轶

(江汉大学 数学与计算机科学学院,湖北 武汉 430056)

基于Node.js的JavaScript并发控制流框架

李轶

(江汉大学 数学与计算机科学学院,湖北 武汉 430056)

Node.js因其异步I/O的特性,非常适合于服务器端的JavaScript开发。然而为实现此环境下异步I/O的并发控制,开发者不得不手工编写繁琐的代码,因而给开发者造成了障碍。以并发计数器为基础,可以设计一个并发控制流框架。该框架以直观的调用形式,实现了异步I/O间的并发控制;其不仅有助于Node.js环境下的JavaScript开发,更提高了开发者的开发效率。

异步I/O;并发;同步;并发计数器;JavaScript

0 引言

时至今日,JavaScript的应用领域早已从前端的Web浏览器,延伸到后台服务器。作为一个基于V8[1]引擎的服务器端JavaScript环境,Node.js[2]从发布之初就备受瞩目。Node.js以V8引擎的轻量、高效、快速为基础,同时又提供了基于事件驱动的异步I/O(Asynchronous I/O)特性[3],使其非常适合于开发不同规模的数据密集型、实时型分布式应用。

1 异步I/O与并发控制

异步I/O又称为非阻塞式I/O,它是Node.js具有的独特特性。不同于传统的每客户一服务线程的服务器架构,Node.js是单线程脚本环境。其核心思想是用一个服务线程应对多个客户请求。当服务进程响应客户请求执行I/O操作(如网络I/O、文件I/O或数据库I/O等)时,异步I/O可避免因服务线程被I/O操作阻塞而无法响应其他用户请求的情况。因此,异步I/O不仅避免了多服务线程对服务器资源的过多占用,又提高了服务线程的执行效率,从而使一个服务线程就能应对大量的并发客户请求。

在调用异步I/O函数时,通常都需要指定一个或多个回调函数作为其参数。这是因为异步I/O的非阻塞特性,异步函数在调用后会立即返回。当I/O操作完成后,需要回调函数通知调用者I/O操作完成。此外,回调函数可能还带有若干参数,其是I/O操作所返回的数据。

有时为实现某一特定功能或为提高程序执行效率,多个异步I/O之间既需要并发(concurrency)[4],又需要在彼此间进行同步(synchronization)[5]。例如,有3个异步I/O可并发执行,但同时要求3个异步I/O全部完成后,才能执行下一步操作。此时,最通常的办法是通过设置并检查多个标志位来实现。假设3个异步I/O函数分别为ioA、ioB和ioC。为达成此目标,需要设置3个I/O标志位,分别为:tagA、tagB、tagC,并将其初值设为false。此后,在每个I/O函数的异步回调中,再将该标识位置设为true,并检查其余标志位。若所有标志位全为true,则表明并发全部完成,可进行下一步操作。例程如下:

由此可以看出,这种复杂繁琐的代码,虽然在执行逻辑上正确,但是在程序编写,代码可读性及程序调试上存在诸多不便。特别是在开发较为复杂逻辑的系统时,随着标志位的增加,复杂繁琐的代码已经成为开发者的阻碍。因此需要一种方法,将复杂的代码简化;也就是说需要一种并发控制流框架,实现异步I/O间的并发控制。

2 任务对象与并发任务缓冲区

为解决上述问题,可考虑应用并发计数器机制。每个异步I/O的调用,可封装成为一个任务对象;依据任务对象的数目,设置并发计数器的初值。当某个任务完成时(即当其I/O函数的回调函数被调用时)将并发计数器值减1;若并发计数器的值减1后为0,则表明所有并发任务均执行完毕。如此,多个异步I/O间的并发控制就可抽象为任务及并发任务计数器的相关操作。

对异步I/O任务而言,其具体属性包括:任务名、任务函数和任务参数;此外,还需要为多个并发任务建立一个并发缓冲区,其状态图如图1所示。并发缓冲区创建后为就绪状态,当需要同步的多个并发任务加入缓冲区后,就可启动并发同步过程。由于任务函数中包含了异步调用,任务函数执行完后会立即返回,但此时并不能认为该任务完成,而是在未来某个时刻,在该任务中的异步回调得到执行后,才可认为该任务执行完毕。在该任务完成后,并发计数器的值会自动减1,并检查其值。若为0则表明所有并发任务均执行完毕,缓冲区返回就绪态。此外,当某任务执行出错时,缓冲区进入终止状态;当某任务超时时,缓冲区进入超时状态。在此两种状态下,都可重新将缓冲区复位为就绪态。

图1 并发任务缓冲区状态图Fig.1 State chart of concurrent task buffer

3 基于计数器的并发控制流框架

以上述讨论作为基础,可构造一个以并发计数器为核心的并发控制流框架。该框架以Node.js的模块形式为用户提供调用接口,模块命名为“concurrentBuf”。另外,由于Node.js为单线程脚本环境,因此也无需考虑计数器操作的线程安全问题。

3.1 模块接口规范(Module interface specifications)

3.1.1 create函数 模块的唯一调用接口为函数“create”,该函数用于创建一个新的并发缓冲区对象。其函数声明为:function create(name,timeoutMs)。

其中参数name为并发缓冲区的名称;timeoutMs为并发任务超时毫秒数,即在所有任务中,最长的任务耗时不得超过timeoutMs的毫秒数;否则认为并发同步超时。

3.1.2 并发任务缓冲区对象的方法 并发任务缓冲区对象有如下方法。

1)push方法

该方法将一个并发任务放入缓冲区。

其方法声明为:push(name,task,argsAry)。

其中参数name为并发任务名;task为任务函数;argsAry为函数task的参数数组,其中每个元素为一个参数对象。参数对象具有属性type和属性value;其中type属性为字符串类型,其用于指示参数的类型。特别需要注意的是,参数的类型除包括JavaScript标准类型之外,还包括类型“callback”,其表示该参数为一回调函数;属性value则用来存储实际的参数值。

2)reset方法

该方法将并发任务缓冲区重新复位,无需任何参数。

3)start方法

该方法启动并发缓冲区中的所有任务,无需任何参数。

4)onError方法

其声明格式为:onError(errorCb)。

该方法用于指定当某个并发任务发生执行错误时的事件处理函数,通过参数errorCB指定。该方法返回并发缓冲区本身的引用。错误事件处理函数errorCb的函数声明为:function(taskName,e)。其中参数taskName为发生错误的任务名;参数e为错误对象。

5)onComplete方法

其声明格式为:onComplete(cb)。

该方法用于指定并发任务全部完成时的事件处理函数,通过参数cb指定。

6)onTimeout方法

其声明格式为:onTimeout(cb)。

该方法用于指定并发任务超时的事件处理函数,通过参数cb指定。

3.1.3 并发任务缓冲区对象的事件 并发任务缓冲区对象有如下事件。

1)error事件

当某个并发任务执行中发生错误时,该事件被触发。该事件的处理函数声明为:function(taskName,e),由缓冲区对象的onError方法指定。

其中参数taskName为发生错误的任务名,参数e为错误对象,其具有的属性包括:code,message和stack;其中code属性为错误代码;message属性为出错信息字符串;stack属性为出错时的函数调用栈。

2)complete事件

当所有并发任务完成时,该事件被触发。该事件处理函数由缓冲区对象的onComplete方法进行指定,该事件无其他参数。

3)timeout事件

当并发任务同步超时时,该事件被触发。该事件处理函数由缓冲区对象的onComplete方法进行指定,该事件无其他参数。

3.2 模块实现

3.2.1 create函数 函数create用于创建一个并发缓冲对象,此函数也是模块对外暴露的唯一接口函数。其函数声明为:function create(name,timeoutMs)。其中参数name为并发缓冲区名称;timeoutMs为任务超时毫秒数。函数的返回值,即为并发缓冲区对象,其具有的方法和事件已经在上述模块接口规范中进行了说明。

3.2.2 并发任务计数器 本框架使用create函数的局部变量pCunter存储引用计数值。由于闭包(closure)[6]的作用,pCunter受到保护,防止了其从外部被访问,并具有和缓冲区对象相同的生存期。

3.2.3 并发任务存储 本框架使用create函数的局部变量pBuffer存储任务队列。pBuffer为一数组,其中的每个成员,都是一个任务对象。任务对象具有3个属性:fName、fn和argary。其中fName为任务名;fn为任务函数;argary为任务函数参数值数组。

同样由于闭包的作用,pBuffer受到保护,防止了其从外部被访问,并具有和缓冲区对象相同的生存期。

3.2.4 并发任务缓冲区状态 本框架使用模块全局对象STATES,枚举缓冲区的所有状态。缓冲区对象的状态转换,如图2所示。对象STATES包含以下属性:ready、running、terminated和expired。属性ready表示缓冲区就绪,其值为0;属性running表示并发任务运行中,其值为1;属性terminated表示缓冲区因某任务执行异常而终止,其值为2;属性expired表示任务执行超时导致缓冲区并发同步超时,其值为3。

图2 并发任务缓冲区对象状态图Fig.2 Object state chart of concurrent task buffer

对于缓冲区状态的保存,则由模块函数create的局部变量pState存储。

3.2.5 并发任务的加入 并发任务的加入由缓冲区对象的push方法完成,具体描述已在模块接口定义规范中进行了说明,执行流程较为简单。push方法首先判断缓冲区的当前状态,若不处于就绪态,则方法直接返回;否则,push方法将任务名、任务函数以及任务参数数组封装成为一个任务对象,并将之追加到pBuffer数组中。

3.2.6 任务对象的封装 如上所述,push方法会将任务名、任务函数以及任务参数数组封装成为一任务对象。在对任务进行封装时,有两个需要注意的地方。

首先,为捕获任务函数中可能发生的执行异常,需要对任务函数进行二次封装。即在原任务函数的基础上构造出一个新的函数,如图3所示。此函数通过try…catch语句调用原任务函数,若发生异常,则在catch子句中终止超时计时器,并改变缓冲区状态为终止态(STATUS.terminated),然后触发缓冲区的error事件,并将捕获的错误对象交由用户进行处理。

其次,为保证并发引用计数器在任务完成后能自动减1,还需要对任务参数数组中的回调函数进行二次封装。图4为任务回调的二次封装函数执行流程。二次封装函数首先检查当前缓冲区状态;若为运行态(STATUS.running),则将计数器减1,然后执行用户的任务回调函数。在用户回调函数返回后,检查计数器的值,若为0则表示所有并发任务均已完成,因此终止超时计时器,并设置缓冲区状态为就绪态,最后触发缓冲区complete事件;若当前缓冲区状态为非运行态,说明有某任务执行异常或超时,并发执行失败,因此直接返回即可。

图3 任务函数的二次封装函数执行流程图Fig.3 Executive flow chart of the second wrapper function of task function

图4 任务回调的二次封装函数执行流程Fig.4 Flow chart of the second wrapper function of task callback

由此可见,任务对象中的任务函数及参数数组中的回调函数都是其原函数的二次封装函数。

3.2.7 并发任务的启动 并发任务的启动由并发缓冲区对象的start方法完成。该方法的执行流程较为简单,首先启动一个任务超时计时器,并设置好其超时回调函数;然后设置引用计数器pCunter的初值为pBuffer数组长度,并设置缓冲区状态为运行态。最后,依次调用pBuffer数组中每个任务对象的任务函数。

3.2.8 并发任务缓冲区复位 并发缓冲区复位由并发缓冲区对象的reset方法实现,其执行流程较为简单。首先检查当前队列状态是否为终止态或超时态。若是,则清空pBuffer数组并更改状态为就绪态(STATUS.ready)。

3.3 模块接口导出

本模块的唯一导出接口为函数create,通过Node.js的模块对象module的exports属性进行导出,示例如下:

module.exports={"create":create};

4 示例

以一个简单的实例演示本框架的使用方法。假设有两个文件读取操作,分别为文件A.txt和B.txt,需要并发控制完成,示例如下:

程序首先通过require函数,分别获得并发同步模块parallelBuffer和Node.js的文件系统模块fs。接着调用parallelBuffer的create函数,创建一个并发同步缓冲区buf;通过指定timeoutMs参数为5 000设定任务的最长超时为5 000 ms;接着调用其onError方法,onTimeout方法和onComplete方法,分别指定了任务在出错、超时和所有任务完成时的事件处理函数。

接下来,通过缓冲区对象的push方法,添加了两个并发任务"read_fileA"及"read_fileB";其任务函数通过Node.js的fs文件系统模块,分别读取文件A.txt和文件B.txt的内容。任务函数的调用参数,被封装为1个数组;本例中的任务函数需要2个参数,因此其参数数组包含2个元素,分别为文件名以及文件读取完毕的回调函数。

最后,程序通过缓冲区对象的start方法启动并发任务。若所有并发任务执行无误,则程序将在控制台输出“所有任务并发同步完成!”。

5 结语

通过并发计数器的应用,在Node.js环境下成功地设计了一个并发控制流框架。解决了因多个异步I/O并发控制所导致的代码繁琐、逻辑复杂等的问题。框架在设计上简洁易用,语义明确清晰,代码可读性强,符合开发者的一般逻辑;同时又采用纯JavaScript实现,不依赖第三方模块或组件,具有较好的通用性,稍加修改就能适用于各种JavaScript环境。

[1]WIKIPEDIA.V8(JavaScript engine)[EB/OL].(2014-6-23)[2014-9-10]http://en.wikipedia.org/wiki/V8_(JavaScript_engine).

[2]WIKIPEDIA.Node.js[EB/OL].(2014-7-10)[2014-9-10]http://en.wikipedia.org/wiki/Node.js.

[3]WIKIPEDIA.Asynchronous I/O[EB/OL].(2014-6-11)[2014-9-10]http://en.wikipedia.org/wiki/Asynchronous_I/O.

[4]WIKIPEDIA.Concurrency(computer science)[EB/OL].(2014-10-11)[2014-10-11]http://en.wikipedia.org/wiki/Concurrency_(computer_science).

[5]WIKIPEDIA.Synchronization(computer science)[EB/OL].(2014-10-15)[2014-10-15]http://en.wikipedia.org/wiki/Synchronization_(computer_science).

[6]WIKIPEDIA.Closure(computer science)[EB/OL].(2014-10-17)[2014-10-17]http://en.wikipedia.org/wiki/Closure_(computer_programming).

[7]BURNHAM T.JavaScript异步编程:设计快速响应的网络应用[M].北京:人民邮电出版社,2013.

(责任编辑:范建凤)

JavaScript Concurrent Flow-Control Framework Based on Node.js

LI Yi
(School of Mathematics and Computer Science,Jianghan University,Wuhan 430056,Hubei,China)

Because of the asynchronous characteristic of I/O,Node.js is quite suitable for developing server-side JavaScript applications.To implement concurrent asynchronous I/O control,developers have to write tedious code manually,it becomes a barrier to developers.Based on the concurrent counter,a concurrent flow-control framework can be constructed.The framework achieved concurrent control between multiple asynchronous I/O in a direct viewing way.Thus,it benefits server-side JavaScript developments and also promotes the efficiency of development.

asynchronous I/O;concurrency;synchronization;concurrent counter;JavaScript

TP393.01

A

1673-0143(2015)02-0170-07

10.16389/j.cnki.cn42-1737/n.2015.02.013

2014-10-27

李 轶(1976—),男,实验员,硕士,研究方向:网络管理。

猜你喜欢

缓冲区计数器调用
采用虚拟计数器的电子式膜式燃气表
核电项目物项调用管理的应用研究
LabWindows/CVI下基于ActiveX技术的Excel调用
基于网络聚类与自适应概率的数据库缓冲区替换*
嫩江重要省界缓冲区水质单因子评价法研究
基于系统调用的恶意软件检测技术研究
计数器竞争冒险及其处理的仿真分析
关键链技术缓冲区的确定方法研究
任意N进制计数器的设计方法
基于单片机的仰卧起坐计数器