反應器設計模式(Reator pattern)是一種基于事件驅動的設計模式梳虽,常用于高并發(fā)場景下宋舷,常見的像Node.js状植、Netty袍嬉、Vert.x中都有著Reactor模式的身影。本文對Reactor模式作了簡要介紹厅贪,結合對比Node.js線程模型進行分析蠢护。
單線程Reactor模式
Reactor模式是一種事件處理模式,單個或多個事件并發(fā)地投遞到事件處理服務养涮,事件處理服務將事件進行分離葵硕,同步的將他們分發(fā)到對應的事件處理器中眉抬。
模式結構
下圖是一張示意圖,結合了維基百科的定義:
- Handle:句柄懈凹,是對資源在操作系統(tǒng)層面的抽象(unix中的fd)蜀变,可供系統(tǒng)輸入或輸出的資源。在網(wǎng)絡編程中介评,一般指的是一個連接库北,如socket,Java NIO中的Channel
- Demultiplexer:同步事件分離器们陆,通常使用EventLoop來進行資源的阻塞等待寒瓦,當一個資源處于就緒狀態(tài)的時候,會被輪詢出來傳遞給分發(fā)器
- Dispatcher:分發(fā)器坪仇,用來注冊杂腰、移除EventHandler,將資源分配到對應的處理器中同步執(zhí)行
- EventHandler:事件處理器烟很,處理對應的事件颈墅,一般由分發(fā)器進行回調。
交互過程
- 初始化Dispatcher
- 注冊EventHandler到Dispatcher雾袱,每個事件處理器包含對應Handle的引用恤筛,這樣就可以建立Handle到EventHandler的映射
- 啟動EventLoop,阻塞等待資源的某個事件發(fā)生
- 當某些Handle的事件發(fā)生后芹橡,資源變?yōu)榫途w狀態(tài)毒坛,會被傳遞給Dispatcher,分發(fā)器通過開始注冊的資源映射關系林说,調用對應的EventHandler方法煎殷。
這就是最簡單的Reactor模式,它其實也是I/O多路復用的一種具現(xiàn)腿箩,用戶不需要考慮并發(fā)的問題豪直,直接交給了事件處理器進行,同時也減少了高并發(fā)情況下多線程對系統(tǒng)資源的消耗珠移。
在一些小容量的場景下弓乙,單線程的模式可以使用,但在高負載钧惧、大并發(fā)的場景下卻不適用暇韧,主要在于一個NIO線程無法支撐同時處理成百上千的鏈路處理,在編解碼消息時浓瞪,由于無法及時處理會造成消息堆積懈玻,進而形成請求超時等問題,而且單個線程也不利于程序的穩(wěn)定性乾颁,一旦線程崩了涂乌,整個程序就崩掉了艺栈。所以在實際應用中大部分都是多線程的Reactor模式。
Node.js線程模型
Node.js是一個采用事件驅動和異步非阻塞I/O骂倘,實現(xiàn)的單線程眼滤、高并發(fā)的JavaScript運行環(huán)境巴席,使用它可以編寫性能良好的web服務端历涝。筆者在學習Vert.x的時候發(fā)現(xiàn)它與Node.js的線程模型很類似,它并不是嚴格意義上的Reactor模式漾唉,這里用來與單線程的Reactor模式作對比荧库,加深理解。
眾所周知赵刑,I/O操作絕大部分都十分耗時分衫,傳統(tǒng)的方法是使用多線程來解決I/O耗時阻塞的問題,多路復用也可以實現(xiàn)同時處理多個任務的功能般此,那么單線程的IO是如何處理I/O的并發(fā)請求呢蚪战?
Node.js并不是單純的單線程,它用主線程處理所有請求铐懊,然后對I/O操作進行異步處理邀桑,交給其他線程去執(zhí)行,避免了頻繁創(chuàng)建科乎、銷毀和上下文切換帶來的系統(tǒng)開銷壁畸。下面來看Node.js的工作原理。
工作原理
從左到右茅茂,從上到下捏萍,Node.js 被分為了四層,分別是 應用層空闲、V8引擎層令杈、Node API層 和 LIBUV層。
- 應用層: 即 JavaScript 交互層碴倾,常見的就是 Node.js 的模塊逗噩,比如 http,fs
- V8引擎層: 即利用 V8 引擎來解析JavaScript 語法影斑,進而和下層 API 交互
- NodeAPI層: 為上層模塊提供系統(tǒng)調用给赞,一般是由 C 語言來實現(xiàn),和操作系統(tǒng)進行交互
- LIBUV層: 是跨平臺的底層封裝矫户,實現(xiàn)了 事件循環(huán)片迅、文件操作等,是 Node.js 實現(xiàn)異步的核心
Node.js在主線程維護了一個事件隊列皆辽,接收到請求后柑蛇,就將該請求作為一個事件放入Event Queue中芥挣,然后繼續(xù)接受其他請求,當主線程空閑(沒有請求接收) 的時候耻台,就開始輪詢事件隊列空免。這里要分兩種情況:
- 普通任務,就由主線程親自執(zhí)行盆耽,并通過回調函數(shù)返回給上層調用
- I/O任務蹋砚,就從線程池中拿出一個線程處理這個事件,指定回調函數(shù)摄杂,繼續(xù)輪詢事件隊列中的其他事件坝咐。當線程中的I/O任務完成以后,執(zhí)行回調函數(shù)析恢,并把這個完成的事件放在事件隊列的尾部墨坚,等待事件循環(huán),當主線程再次循環(huán)到該完成事件時映挂,再返回給上層調用泽篮。
Node.js的單線程并非整個環(huán)境都運行在單線程中,而是對JavaScript層面的任務處理是單線程的柑船。
像Node.js這種解決方案:將耗時少的短任務交給主線程來處理帽撑,將I/O操作或者一些CPU密集型任務交給其他線程來執(zhí)行,這樣不會阻塞EventLoop正常進行事件的循環(huán)椎组,是比較通用的解決方案油狂。例如在Vert.x中,對于普通的verticle寸癌,會運行在EventLoop線程中专筷,對于耗時長的任務則放在Worker Pool中的線程上運行。
多線程下的Reactor模式與Node.js這種解決方案很類似蒸苇,一個線程負責鏈路的監(jiān)聽磷蛹、建立,然后將I/O交給其它子線程來完成溪烤。后文會有介紹味咳。