李軼
(江漢大學 數學與計算機科學學院,湖北 武漢 430056)
基于Node.js的JavaScript并發(fā)控制流框架
李軼
(江漢大學 數學與計算機科學學院,湖北 武漢 430056)
Node.js因其異步I/O的特性,非常適合于服務器端的JavaScript開發(fā)。然而為實現此環(huán)境下異步I/O的并發(fā)控制,開發(fā)者不得不手工編寫繁瑣的代碼,因而給開發(fā)者造成了障礙。以并發(fā)計數器為基礎,可以設計一個并發(fā)控制流框架。該框架以直觀的調用形式,實現了異步I/O間的并發(fā)控制;其不僅有助于Node.js環(huán)境下的JavaScript開發(fā),更提高了開發(fā)者的開發(fā)效率。
異步I/O;并發(fā);同步;并發(fā)計數器;JavaScript
時至今日,JavaScript的應用領域早已從前端的Web瀏覽器,延伸到后臺服務器。作為一個基于V8[1]引擎的服務器端JavaScript環(huán)境,Node.js[2]從發(fā)布之初就備受矚目。Node.js以V8引擎的輕量、高效、快速為基礎,同時又提供了基于事件驅動的異步I/O(Asynchronous I/O)特性[3],使其非常適合于開發(fā)不同規(guī)模的數據密集型、實時型分布式應用。
異步I/O又稱為非阻塞式I/O,它是Node.js具有的獨特特性。不同于傳統(tǒng)的每客戶一服務線程的服務器架構,Node.js是單線程腳本環(huán)境。其核心思想是用一個服務線程應對多個客戶請求。當服務進程響應客戶請求執(zhí)行I/O操作(如網絡I/O、文件I/O或數據庫I/O等)時,異步I/O可避免因服務線程被I/O操作阻塞而無法響應其他用戶請求的情況。因此,異步I/O不僅避免了多服務線程對服務器資源的過多占用,又提高了服務線程的執(zhí)行效率,從而使一個服務線程就能應對大量的并發(fā)客戶請求。
在調用異步I/O函數時,通常都需要指定一個或多個回調函數作為其參數。這是因為異步I/O的非阻塞特性,異步函數在調用后會立即返回。當I/O操作完成后,需要回調函數通知調用者I/O操作完成。此外,回調函數可能還帶有若干參數,其是I/O操作所返回的數據。
有時為實現某一特定功能或為提高程序執(zhí)行效率,多個異步I/O之間既需要并發(fā)(concurrency)[4],又需要在彼此間進行同步(synchronization)[5]。例如,有3個異步I/O可并發(fā)執(zhí)行,但同時要求3個異步I/O全部完成后,才能執(zhí)行下一步操作。此時,最通常的辦法是通過設置并檢查多個標志位來實現。假設3個異步I/O函數分別為ioA、ioB和ioC。為達成此目標,需要設置3個I/O標志位,分別為:tagA、tagB、tagC,并將其初值設為false。此后,在每個I/O函數的異步回調中,再將該標識位置設為true,并檢查其余標志位。若所有標志位全為true,則表明并發(fā)全部完成,可進行下一步操作。例程如下:
由此可以看出,這種復雜繁瑣的代碼,雖然在執(zhí)行邏輯上正確,但是在程序編寫,代碼可讀性及程序調試上存在諸多不便。特別是在開發(fā)較為復雜邏輯的系統(tǒng)時,隨著標志位的增加,復雜繁瑣的代碼已經成為開發(fā)者的阻礙。因此需要一種方法,將復雜的代碼簡化;也就是說需要一種并發(fā)控制流框架,實現異步I/O間的并發(fā)控制。
為解決上述問題,可考慮應用并發(fā)計數器機制。每個異步I/O的調用,可封裝成為一個任務對象;依據任務對象的數目,設置并發(fā)計數器的初值。當某個任務完成時(即當其I/O函數的回調函數被調用時)將并發(fā)計數器值減1;若并發(fā)計數器的值減1后為0,則表明所有并發(fā)任務均執(zhí)行完畢。如此,多個異步I/O間的并發(fā)控制就可抽象為任務及并發(fā)任務計數器的相關操作。
對異步I/O任務而言,其具體屬性包括:任務名、任務函數和任務參數;此外,還需要為多個并發(fā)任務建立一個并發(fā)緩沖區(qū),其狀態(tài)圖如圖1所示。并發(fā)緩沖區(qū)創(chuàng)建后為就緒狀態(tài),當需要同步的多個并發(fā)任務加入緩沖區(qū)后,就可啟動并發(fā)同步過程。由于任務函數中包含了異步調用,任務函數執(zhí)行完后會立即返回,但此時并不能認為該任務完成,而是在未來某個時刻,在該任務中的異步回調得到執(zhí)行后,才可認為該任務執(zhí)行完畢。在該任務完成后,并發(fā)計數器的值會自動減1,并檢查其值。若為0則表明所有并發(fā)任務均執(zhí)行完畢,緩沖區(qū)返回就緒態(tài)。此外,當某任務執(zhí)行出錯時,緩沖區(qū)進入終止狀態(tài);當某任務超時時,緩沖區(qū)進入超時狀態(tài)。在此兩種狀態(tài)下,都可重新將緩沖區(qū)復位為就緒態(tài)。
圖1 并發(fā)任務緩沖區(qū)狀態(tài)圖Fig.1 State chart of concurrent task buffer
以上述討論作為基礎,可構造一個以并發(fā)計數器為核心的并發(fā)控制流框架。該框架以Node.js的模塊形式為用戶提供調用接口,模塊命名為“concurrentBuf”。另外,由于Node.js為單線程腳本環(huán)境,因此也無需考慮計數器操作的線程安全問題。
3.1 模塊接口規(guī)范(Module interface specifications)
3.1.1 create函數 模塊的唯一調用接口為函數“create”,該函數用于創(chuàng)建一個新的并發(fā)緩沖區(qū)對象。其函數聲明為:function create(name,timeoutMs)。
其中參數name為并發(fā)緩沖區(qū)的名稱;timeoutMs為并發(fā)任務超時毫秒數,即在所有任務中,最長的任務耗時不得超過timeoutMs的毫秒數;否則認為并發(fā)同步超時。
3.1.2 并發(fā)任務緩沖區(qū)對象的方法 并發(fā)任務緩沖區(qū)對象有如下方法。
1)push方法
該方法將一個并發(fā)任務放入緩沖區(qū)。
其方法聲明為:push(name,task,argsAry)。
其中參數name為并發(fā)任務名;task為任務函數;argsAry為函數task的參數數組,其中每個元素為一個參數對象。參數對象具有屬性type和屬性value;其中type屬性為字符串類型,其用于指示參數的類型。特別需要注意的是,參數的類型除包括JavaScript標準類型之外,還包括類型“callback”,其表示該參數為一回調函數;屬性value則用來存儲實際的參數值。
2)reset方法
該方法將并發(fā)任務緩沖區(qū)重新復位,無需任何參數。
3)start方法
該方法啟動并發(fā)緩沖區(qū)中的所有任務,無需任何參數。
4)onError方法
其聲明格式為:onError(errorCb)。
該方法用于指定當某個并發(fā)任務發(fā)生執(zhí)行錯誤時的事件處理函數,通過參數errorCB指定。該方法返回并發(fā)緩沖區(qū)本身的引用。錯誤事件處理函數errorCb的函數聲明為:function(taskName,e)。其中參數taskName為發(fā)生錯誤的任務名;參數e為錯誤對象。
5)onComplete方法
其聲明格式為:onComplete(cb)。
該方法用于指定并發(fā)任務全部完成時的事件處理函數,通過參數cb指定。
6)onTimeout方法
其聲明格式為:onTimeout(cb)。
該方法用于指定并發(fā)任務超時的事件處理函數,通過參數cb指定。
3.1.3 并發(fā)任務緩沖區(qū)對象的事件 并發(fā)任務緩沖區(qū)對象有如下事件。
1)error事件
當某個并發(fā)任務執(zhí)行中發(fā)生錯誤時,該事件被觸發(fā)。該事件的處理函數聲明為:function(taskName,e),由緩沖區(qū)對象的onError方法指定。
其中參數taskName為發(fā)生錯誤的任務名,參數e為錯誤對象,其具有的屬性包括:code,message和stack;其中code屬性為錯誤代碼;message屬性為出錯信息字符串;stack屬性為出錯時的函數調用棧。
2)complete事件
當所有并發(fā)任務完成時,該事件被觸發(fā)。該事件處理函數由緩沖區(qū)對象的onComplete方法進行指定,該事件無其他參數。
3)timeout事件
當并發(fā)任務同步超時時,該事件被觸發(fā)。該事件處理函數由緩沖區(qū)對象的onComplete方法進行指定,該事件無其他參數。
3.2 模塊實現
3.2.1 create函數 函數create用于創(chuàng)建一個并發(fā)緩沖對象,此函數也是模塊對外暴露的唯一接口函數。其函數聲明為:function create(name,timeoutMs)。其中參數name為并發(fā)緩沖區(qū)名稱;timeoutMs為任務超時毫秒數。函數的返回值,即為并發(fā)緩沖區(qū)對象,其具有的方法和事件已經在上述模塊接口規(guī)范中進行了說明。
3.2.2 并發(fā)任務計數器 本框架使用create函數的局部變量pCunter存儲引用計數值。由于閉包(closure)[6]的作用,pCunter受到保護,防止了其從外部被訪問,并具有和緩沖區(qū)對象相同的生存期。
3.2.3 并發(fā)任務存儲 本框架使用create函數的局部變量pBuffer存儲任務隊列。pBuffer為一數組,其中的每個成員,都是一個任務對象。任務對象具有3個屬性:fName、fn和argary。其中fName為任務名;fn為任務函數;argary為任務函數參數值數組。
同樣由于閉包的作用,pBuffer受到保護,防止了其從外部被訪問,并具有和緩沖區(qū)對象相同的生存期。
3.2.4 并發(fā)任務緩沖區(qū)狀態(tài) 本框架使用模塊全局對象STATES,枚舉緩沖區(qū)的所有狀態(tài)。緩沖區(qū)對象的狀態(tài)轉換,如圖2所示。對象STATES包含以下屬性:ready、running、terminated和expired。屬性ready表示緩沖區(qū)就緒,其值為0;屬性running表示并發(fā)任務運行中,其值為1;屬性terminated表示緩沖區(qū)因某任務執(zhí)行異常而終止,其值為2;屬性expired表示任務執(zhí)行超時導致緩沖區(qū)并發(fā)同步超時,其值為3。
圖2 并發(fā)任務緩沖區(qū)對象狀態(tài)圖Fig.2 Object state chart of concurrent task buffer
對于緩沖區(qū)狀態(tài)的保存,則由模塊函數create的局部變量pState存儲。
3.2.5 并發(fā)任務的加入 并發(fā)任務的加入由緩沖區(qū)對象的push方法完成,具體描述已在模塊接口定義規(guī)范中進行了說明,執(zhí)行流程較為簡單。push方法首先判斷緩沖區(qū)的當前狀態(tài),若不處于就緒態(tài),則方法直接返回;否則,push方法將任務名、任務函數以及任務參數數組封裝成為一個任務對象,并將之追加到pBuffer數組中。
3.2.6 任務對象的封裝 如上所述,push方法會將任務名、任務函數以及任務參數數組封裝成為一任務對象。在對任務進行封裝時,有兩個需要注意的地方。
首先,為捕獲任務函數中可能發(fā)生的執(zhí)行異常,需要對任務函數進行二次封裝。即在原任務函數的基礎上構造出一個新的函數,如圖3所示。此函數通過try…catch語句調用原任務函數,若發(fā)生異常,則在catch子句中終止超時計時器,并改變緩沖區(qū)狀態(tài)為終止態(tài)(STATUS.terminated),然后觸發(fā)緩沖區(qū)的error事件,并將捕獲的錯誤對象交由用戶進行處理。
其次,為保證并發(fā)引用計數器在任務完成后能自動減1,還需要對任務參數數組中的回調函數進行二次封裝。圖4為任務回調的二次封裝函數執(zhí)行流程。二次封裝函數首先檢查當前緩沖區(qū)狀態(tài);若為運行態(tài)(STATUS.running),則將計數器減1,然后執(zhí)行用戶的任務回調函數。在用戶回調函數返回后,檢查計數器的值,若為0則表示所有并發(fā)任務均已完成,因此終止超時計時器,并設置緩沖區(qū)狀態(tài)為就緒態(tài),最后觸發(fā)緩沖區(qū)complete事件;若當前緩沖區(qū)狀態(tài)為非運行態(tài),說明有某任務執(zhí)行異常或超時,并發(fā)執(zhí)行失敗,因此直接返回即可。
圖3 任務函數的二次封裝函數執(zhí)行流程圖Fig.3 Executive flow chart of the second wrapper function of task function
圖4 任務回調的二次封裝函數執(zhí)行流程Fig.4 Flow chart of the second wrapper function of task callback
由此可見,任務對象中的任務函數及參數數組中的回調函數都是其原函數的二次封裝函數。
3.2.7 并發(fā)任務的啟動 并發(fā)任務的啟動由并發(fā)緩沖區(qū)對象的start方法完成。該方法的執(zhí)行流程較為簡單,首先啟動一個任務超時計時器,并設置好其超時回調函數;然后設置引用計數器pCunter的初值為pBuffer數組長度,并設置緩沖區(qū)狀態(tài)為運行態(tài)。最后,依次調用pBuffer數組中每個任務對象的任務函數。
3.2.8 并發(fā)任務緩沖區(qū)復位 并發(fā)緩沖區(qū)復位由并發(fā)緩沖區(qū)對象的reset方法實現,其執(zhí)行流程較為簡單。首先檢查當前隊列狀態(tài)是否為終止態(tài)或超時態(tài)。若是,則清空pBuffer數組并更改狀態(tài)為就緒態(tài)(STATUS.ready)。
3.3 模塊接口導出
本模塊的唯一導出接口為函數create,通過Node.js的模塊對象module的exports屬性進行導出,示例如下:
module.exports={"create":create};
以一個簡單的實例演示本框架的使用方法。假設有兩個文件讀取操作,分別為文件A.txt和B.txt,需要并發(fā)控制完成,示例如下:
程序首先通過require函數,分別獲得并發(fā)同步模塊parallelBuffer和Node.js的文件系統(tǒng)模塊fs。接著調用parallelBuffer的create函數,創(chuàng)建一個并發(fā)同步緩沖區(qū)buf;通過指定timeoutMs參數為5 000設定任務的最長超時為5 000 ms;接著調用其onError方法,onTimeout方法和onComplete方法,分別指定了任務在出錯、超時和所有任務完成時的事件處理函數。
接下來,通過緩沖區(qū)對象的push方法,添加了兩個并發(fā)任務"read_fileA"及"read_fileB";其任務函數通過Node.js的fs文件系統(tǒng)模塊,分別讀取文件A.txt和文件B.txt的內容。任務函數的調用參數,被封裝為1個數組;本例中的任務函數需要2個參數,因此其參數數組包含2個元素,分別為文件名以及文件讀取完畢的回調函數。
最后,程序通過緩沖區(qū)對象的start方法啟動并發(fā)任務。若所有并發(fā)任務執(zhí)行無誤,則程序將在控制臺輸出“所有任務并發(fā)同步完成!”。
通過并發(fā)計數器的應用,在Node.js環(huán)境下成功地設計了一個并發(fā)控制流框架。解決了因多個異步I/O并發(fā)控制所導致的代碼繁瑣、邏輯復雜等的問題??蚣茉谠O計上簡潔易用,語義明確清晰,代碼可讀性強,符合開發(fā)者的一般邏輯;同時又采用純JavaScript實現,不依賴第三方模塊或組件,具有較好的通用性,稍加修改就能適用于各種JavaScript環(huán)境。
(
)
[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—),男,實驗員,碩士,研究方向:網絡管理。