新聞中心
因此NodeJS的基本模式是,由一個主線程不斷接收客戶端請求,如果請求需要一定時間才完成,主線程會將任務丟給線程池,然后繼續(xù)回頭處理其他客戶的請求。在主線程的循環(huán)中,它會不斷輪詢特定隊列,看看是否有數(shù)據(jù)可以處理,如果有那么它就從隊列中取下來,然后將數(shù)據(jù)進行處理后發(fā)送給需要的客戶端。由于主線程不用長時間阻塞,因此它能夠在給定時間內(nèi)對大量的客戶端請求進行響應,這是它能實現(xiàn)高并發(fā)的原因。

為察雅等地區(qū)用戶提供了全套網(wǎng)頁設計制作服務,及察雅網(wǎng)站建設行業(yè)解決方案。主營業(yè)務為成都網(wǎng)站設計、做網(wǎng)站、察雅網(wǎng)站設計,以傳統(tǒng)方式定制建設網(wǎng)站,并提供域名空間備案等一條龍服務,秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務。我們深信只要達到每一位用戶的要求,就會得到認可,從而選擇與我們長期合作。這樣,我們也可以走得更遠!
主線程不斷輪詢特定隊列是否有數(shù)據(jù)的過程也叫event loop。其基本流程如下:
NodeJS代碼的特點在于,任何我們自己寫的代碼,它在執(zhí)行時一定在主線程中,而且你不用擔心因多線程導致的重入等問題。在NodeJS代碼中,一旦有異步調(diào)用產(chǎn)生,執(zhí)行流就會將這個調(diào)用提交給它的線程池,然后直接指向異步調(diào)用后面的代碼,例如:
console.log(1)
setTimer(()=>{console.log(2), 0)
console.log(3)
上面代碼運行時輸出結果是1,3,2,這是因為setTimer是異步函數(shù),在主線程里不會得到執(zhí)行,主線程會把這個時鐘任務交給線程池,等到時鐘結束后,里面的回調(diào)就會放置在上圖中的時鐘隊列,因此主線程會越過setTimer直接指向它后面的語句,等到主線程下次循環(huán)到上圖中的時鐘隊列位置時才會把setTimer設置的回調(diào)函數(shù)拿出來執(zhí)行。
由此對于NodeJS的event loop來說它包含若干個階段,每個階段對應上圖的一個方塊。在每個階段,主線程會從對應隊列中獲取數(shù)據(jù)返回給客戶端,或者是將存儲在隊列中的回調(diào)函數(shù)進行執(zhí)行,當隊列清空,或者訪問的隊列元素超過給定值后就會進入下一個階段。
從上圖可以看出,所有時鐘相關的回調(diào)都在Timer階段執(zhí)行,例如代碼使用setTimer, setInterval等接口時,NodeJS會把時鐘請求提交給操作系統(tǒng),一旦時鐘結束后,操作系統(tǒng)會通知NodeJS,后者就會把時鐘對應的回調(diào)掛入Timer階段對應的隊列。第二個階段是操作系統(tǒng)在某項情況下需要通知特定事件給NodeJS,例如TCP連接請求被拒絕,數(shù)據(jù)庫連接失敗等;idle階段屬于nodejs內(nèi)部使用,主線程會執(zhí)行一些nodejs內(nèi)部特定回調(diào)函數(shù)執(zhí)行一些內(nèi)部事務,這部分通常與我們開發(fā)無關;poll階段應該是nodejs主線程的主要工作所在,當文件打開成功,數(shù)據(jù)從文件中讀入,或者數(shù)據(jù)寫入文件等相應IO事件發(fā)生時,對應的回調(diào)函數(shù)都會存儲在這個階段的隊列,典型的fs.writeFile(p, (err, data)=>{})調(diào)用,它對應的回調(diào)函數(shù)就在這個階段才能執(zhí)行。check階段執(zhí)行由setImmediate提交的回調(diào)函數(shù),setImmediate和setTimeout(callback, 0)其實性質(zhì)一樣,只不過這兩個異步函數(shù)對應的回調(diào)在不同的階段執(zhí)行,如果我們再代碼中同時執(zhí)行setImmediate和setTimeout(callback, 0),那么哪個回調(diào)先執(zhí)行就取決于主線程當前處于哪個階段,我們可以做個實驗,在本地創(chuàng)建一個文件例如hello.txt,然后創(chuàng)建index.js,在里面添加代碼如下:
setTimeout(function() {
console.log('setTimeout')
}, 0)
setImmediate(function() {
console.log('setImmediate')
})在多次運行index.js情況下,有時候setTimeout先打印,有時候setImmediate先打印,這取決于主線程處于哪個階段,如果它執(zhí)行時主線程已經(jīng)越過check階段,那么setTimeout將先打印,反之亦然。如果我們在IO回調(diào)中執(zhí)行上面代碼,例如:
fs.readFile('./hello.txt', ()=> {
setTimeout(function() {
console.log('setTimeout in read file')
}, 0)
setImmediate(function() {
console.log('setImmediate in read file')
})
})那么setImmediate in read file一定會先打印,因為readFile的回調(diào)在poll階段執(zhí)行,而check階段緊跟著poll,因此讀取文件的回調(diào)執(zhí)行后主線程進入check階段,于是setImmediate設置的回調(diào)一定先執(zhí)行。
上圖中還有一個process.nextTick,它也是一個異步函數(shù),但它不屬于event loop的任何階段,當當前event loop階段走完重新回到timer階段時,主線程會先查看是否有nextTick提供的回調(diào),如果有,那么先執(zhí)行給定回調(diào)然后再進入timer階段。它本質(zhì)上跟setImmediate沒有什么區(qū)別,只不過后者屬于event loop的特定階段而前者不屬于event loop,因此它最大的作用是讓代碼在主線程進入下一輪循環(huán)前做一些操作,例如釋放掉一些沒用的資源。
由于nodejs的異步模式,有些錯誤可能很難處理,這類問題稱之為Zalgo問題,他們的特點是把同步邏輯和異步邏輯組合在一起從而導致難以復現(xiàn)和難以調(diào)試的Bug,一個例子如下:
import {readFile} from 'fs'
const cache = new Map()
function problemRead(filename, cb) {
if (cache.has(filename)) {
cb(cache.get(filename))
} else {
readFile(filename, 'utf8', (err, data)= {
cache.set(filename, data)
cb(data)
})
}
}在上面代碼中,problemRead有兩種模式,一種是如果緩存沒有存在,那么使用readFile進行異步讀取,如果緩存已經(jīng)存在,那么cb對應的回調(diào)函數(shù)將直接執(zhí)行,因此cb有可能在執(zhí)行時存在不同上下文環(huán)境,這種情況很容易導致代碼出現(xiàn)問題,例如創(chuàng)建文件zalgo.mjs,實現(xiàn)代碼如下:
function createFileReader(filename) {
const listeners = []
problemRead(filename, value=>{
listeners.forEach(listener => listener(value))
})
return {
onDataReady: listener => listeners.push(listener)
}
}
const reader1 = createFileReader('./hello.txt')
reader1.onDataReady(data => {
console.log("calling from reader1: ", data)
const reader2 = createFileReader('./hello.txt')
reader2.onDataReady(data => {
//這里的回調(diào)不會被調(diào)用
console.log('calling from reader2: ', data)
})
})上面代碼執(zhí)行時只會輸出:
calling from reader1: hello world!
也就是read2對應的回調(diào)沒有調(diào)用。它的原因是這樣,第一次調(diào)用createFileReader時,由于數(shù)據(jù)沒有緩存,因此代碼調(diào)用異步接口readFile,前面我們說過任何異步調(diào)用都會提交內(nèi)線程池,它絕不會在主線程中運行,因此readFile接下來的代碼會直接運行,于是我們就有機會把reader1對應的回調(diào)加入到listeners隊列,等到回調(diào)完成后,reader1的回調(diào)函數(shù)已經(jīng)存儲在listeners中,于是在回調(diào)中遍歷listeners隊列,取出其中的回調(diào)函數(shù)執(zhí)行,這樣reader1指定的回調(diào)就能得以執(zhí)行。
在reader2對應的createFileReader函數(shù)執(zhí)行后,對應的數(shù)據(jù)已經(jīng)存儲在緩存中,于是代碼直接將listener2隊列中的回調(diào)元素拿出來執(zhí)行,注意這個時候reader2.onDataReady對應代碼還沒有執(zhí)行,因此reader2對應的回調(diào)函數(shù)還沒有來得及放入到listeners隊列,于是它就得不到執(zhí)行的機會。這種問題很難調(diào)試,首先它不好重現(xiàn),如果createReader后面繼續(xù)存在被調(diào)用,那么reader2對應的回調(diào)就可以被執(zhí)行,同時上面代碼reader2的回調(diào)沒有執(zhí)行,同時代碼也不產(chǎn)生任何異?;蝈e誤,這使得問題的定位會非常困難,nodejs社區(qū)把這種問題叫做upleasing zalgo,這是一個特定的典故。這給我們的教訓是,在代碼中要不全部使用異步模式,要不就同步模式,決不能兩種交叉混合使用。
文章題目:Nodejs深度探秘:EventLoop的本質(zhì)和異步代碼中的Zalgo問題
分享網(wǎng)址:http://fisionsoft.com.cn/article/cojoiog.html


咨詢
建站咨詢
