①常見的IO模型:Linux(UNIX)操做系統中的網絡IO模型爲例
node
Blocking I/O
同步阻塞IONon-blocking I/O
同步非阻塞IOI/O Multiplexing
IO多路複用Signal-blocking I/O
信號驅動IOAsynchronous I/O
異步IO②基本概念的定義:linux
IO 指的是輸入輸出,一般指數據在內部存儲器和外部存儲器或其餘周邊設備之間的輸入和輸出。簡而言之,從硬盤中讀寫數據或者從網絡上收發數據,都屬於IO行爲。編程
③完成一次IO的過程: 以讀一個文件爲例,一個IO讀過程是文件數據從磁盤→內核緩衝區→用戶內存的過程。segmentfault
同步與異步的區別主要在於數據從內核緩衝區→用戶內存這個過程需不須要用戶(應用)進程等待,即實際的IO讀寫是否阻塞請求進程。(網絡IO可把磁盤換作網卡)
windows
阻塞 I/O
是最簡單的 I/O 模型,通常表現爲進程或線程等待某個條件,若是條件不知足,則一直等下去。條件知足,則進行下一步操做。promise
應用進程經過系統調用 recvfrom
接收數據,但因爲內核還未準備好數據報,應用進程就會阻塞住,直到內核準備好數據報,recvfrom
完成數據報復制工做,應用進程才能結束阻塞狀態。瀏覽器
應用進程經過 recvfrom
調用不停的去和內核交互,直到內核準備好數據。若是沒有準備好,內核會返回 error
,應用進程在獲得 error
後,過一段時間再發送 recvfrom
請求。若是某一次輪詢發現數據已經準備好了,那就把數據拷貝到用戶空間中。在發送請求的時間間隔中,進程能夠先作別的事情。安全
IO多路複用是多了一個select
函數,多個進程的IO能夠註冊到同一個select
上,當用戶進程調用該select
,select
會監聽全部註冊好的IO,若是全部被監聽的IO須要的數據都沒有準備好時,select
調用進程會阻塞。當任意一個IO所需的數據準備好以後,select
調用就會返回,而後進程在經過recvfrom
來進行數據拷貝。bash
這裏的IO複用模型,並無向內核註冊信號處理函數,因此,他並非非阻塞的。進程在發出select後,要等到select監聽的全部IO操做中至少有一個須要的數據準備好,纔會有返回,而且也須要再次發送請求去進行文件的拷貝。服務器
應用進程預先向內核註冊一個信號處理函數,而後用戶進程返回,而且不阻塞,當內核數據準備就緒時會發送一個信號給進程,用戶進程便在信號處理函數中開始把數據拷貝的用戶空間中。
應用進程發起aio_read
操做以後,給內核傳遞描述符、緩衝區指針、緩衝區大小等,告訴內核當整個操做完成時,如何通知進程,而後就馬上去作其餘事情了。當內核收到aio_read
後,會馬上返回,而後內核開始等待數據準備,數據準備好之後,直接把數據拷貝到用戶控件,而後再通知進程本次IO已經完成。
阻塞IO模型、非阻塞IO模型、IO多路複用和信號驅動IO模型都是同步的IO模型,由於不管以上那種模型,真正的數據拷貝過程,都是同步進行的。
libuv是一個高性能事件驅動庫,屏蔽了各類操做系統的差別從而提供了統一的API。libuv嚴格使用異步、事件驅動的編程風格。其核心工做是提供事件循環及 基於I/O
或其餘活動事件的回調機制。libuv庫包含了諸如計時器、非阻塞網絡支持、異步文件系統訪問、線程建立、子進程等核心工具。
libuv給用戶提供了兩種方式與event loop一塊兒協同工做,一個是句柄(handle)一個是請求(request)。
句柄(handle)表明了一個長期存在的對象,這些對象當處於活躍狀態的時候可以執行特定的操做。例如:一個準備(prepare)句柄在活躍的時候能夠在每一個循環中調用它的回調一次。一個TCP服務器的句柄在每次有新的鏈接的時候都會調用它的鏈接回調函數。
請求(request)通常表明短時操做。這些操做能用做用於句柄之上。寫請求用於在句柄上寫數據;還有一些例外,好比說getaddrinfo請求不須要句柄而是直接在循環中執行。
I/O循環或者叫作事件循環是整個libuv的核心部分。I/O循環創建了全部IO操做的執行環境,I/O循環會被綁定在一個線程之上。咱們能夠運行多個事件循環,只要每個都運行在不一樣的線程之上。libuv事件循環不是線程安全的,因此全部包含事件循環的API及句柄都不是線程安全的。
事件循環遵循最廣泛的單線程異步I/O
方法:全部I/O或者網絡操做在非阻塞的socket上執行,這個socket會使用基於平臺的組好的poll機制:在linux上使用epoll
,在OSX和其餘BSD平臺上使用kqueue
,在sunOS上使用event ports
,在windows上使用IOCP
。做爲循環迭代的一部分,循環會阻塞以等待socket上的I/O活動,這些活動已經被加到socket的觸發實踐中了,一旦這些條件知足,那麼socket的狀態就會發生變化,從而循環再也不阻塞,並且句柄也能夠讀、寫及執行其餘指望的I/O操做。
更好的理解事件循環操做如何進行,下圖展現了一個循環迭代的全部階段。
文件 I/O 與網絡 I/O 不一樣 ,並不存在 libuv 能夠依靠的各特定平臺下的文件 I/O 基礎函數,因此目前的實現是在線程中執行阻塞的文件 I/O 操做來模擬異步。
注意:libuv利用線程池技術使得異步文件I/O操做稱爲可能,可是對於網絡IO只能執行在一個單一線程中,即loop的線程中。
任務隊列
異步任務分爲task(宏任務,也可稱爲macroTask)和microtask(微任務)兩類。 當知足執行條件時,task和microtask會被放入各自的隊列中等待放入主線程執行,咱們把這兩個隊列稱爲Task Queue(Macrotask Queue)和Microtask Queue。
MacroTask(宏任務)
script代碼
、setTimeout
、setInterval
、setImmediate(瀏覽器IE10)
、MessageChannel
、I/O
、UI-Rendering
MicroTask(微任務)
Process.nextTick(Node獨有)
、Promise
、MutationObserver
、Object.observe(廢棄)
JS調用棧採用的是先進後出
原則,當函數執行的時候,會被添加到棧的頂部,當執行棧執行完成後,就會從棧頂移出,直到棧內被清空。
執行棧在執行完同步任務後
,查看執行棧是否爲空,若是執行棧爲空,就會去檢查微任務microTask
隊列是否爲空,若是爲空的話,就執行Task(宏任務),不然執行微任務。每當單個宏任務執行完畢後
,檢查microTask
隊列是否爲空,若是不爲空,會按照 先入先出
原則所有執行microTask隊列,設置microTask隊列爲null,而後再執行宏任務,如此反覆。console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// script start、script end、promise一、promise二、setTimeout
複製代碼
Another One
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('script end')
複製代碼
async/await
在底層轉換成了 promise
和 then
回調函數。await
, 解釋器都建立一個 promise
對象,而後把剩下的 async
函數中代碼的操做放到 then
回調函數中。關於Chrome73如下版本和73版本的區別
在老版本版本如下,先執行promise1
和promise2
,再執行async1
。 script start、async2 end、Promise、script end、promise一、promise二、async1 end
、setTimeout
在73版中,先執行async1
再執行promise1
和promise2
。 script start、async2 end、Promise、script end、async1 end、promise一、promise2
、setTimeout
主要緣由是由於在谷歌73版本中更改了規範
在Node中事件每一輪循環按照順序分爲6個階段,來自libuv的實現:
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
複製代碼
timers:執行知足條件的setTimeout、setInterval回調。
I/O callbacks:是否有已完成的I/O操做的回調函數,來自上一輪的poll殘留。
idle,prepare:可忽略
poll:等待還沒完成的I/O事件,會因timers和超時時間等結束等待。
check:執行setImmediate的回調。
close callbacks:關閉全部的closing handles,一些onclose事件。
複製代碼
咱們須要重點關心的是timers
、poll
、check
這三個階段。
1. timers 執行setTimeout
和setInterval
中到期的callback
,執行這二者回調須要設置一個毫秒數,理論上來講,應該是時間一到就當即執行
callback回調,可是因爲system的調度可能會延時,達不到預期時間。
2. poll 執行I/O回調
和 處理輪詢隊列
中的事件。
① 若是 poll 隊列不是空的,event loop 就會依次執行隊列裏的回調函數,直到隊列被清空或者到達 poll 階段的時間上限。
② 若是 poll 隊列是空的,就會:
3. check 此階段容許人員在poll階段完成後當即執行回調。
setImmediate()其實是一個特殊的計時器,它在事件循環的一個單獨階段運行。它是經過 libuv 裏一個能將回調安排在 poll 階段以後執行的 API 實現的。
在poll隊列是空的 且有 setImmediate 任務的狀況下,event loop 就結束 poll 階段去往 check 階段執行任務。
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
複製代碼
若是node
版本爲v11.x
, 其結果與瀏覽器一致:
start
end
promise3
timer1
promise1
timer2
promise2
複製代碼
v10
若是time2定時器已經在執行隊列中結果爲:
start
end
promise3
timer1
timer2
promise1
promise2
複製代碼
不然和第一個結果一致。
瞭解瀏覽器的eventloop可能就知道,瀏覽器的宏任務隊列執行了一個,就會執行微任務。
簡單的說,能夠把瀏覽器的宏任務和node10
的timers
比較,就是node10只有所有執行了timers階段隊列的所有任務才執行微任務隊列,而瀏覽器只要執行了一個宏任務就會執行微任務隊列。
node11
保持和瀏覽器相同。
setImmediate和setTimeout是類似的,但根據它們被調用的時間以不一樣的方式表現。
setImmediate()
設計用於在當前poll
階段完成後check
階段執行腳本 。 setTimeout()
爲最小(ms)後運行的腳本,在timers
階段執行。
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
// timeout,immediate
// immediate,timeout
複製代碼
const fs = require('fs');
fs.readFile('../file.txt', () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// immediate,timeout
複製代碼
process.nextTick()
雖然它是異步API的一部分,但從技術上講,它不是事件循環的一部分。
process.nextTick()
方法將 callback 添加到next tick隊列。 一旦當前事件輪詢隊列的任務所有完成,在next tick隊列中的全部callbacks會被依次調用。
當每一個階段完成後,若是存在 nextTick 隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask
執行。
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
// nextTick
// Promise
複製代碼
setImmediate(() => {
console.log('setImmediate1');
setTimeout(() => {
console.log('setTimeout1');
}, 0);
});
setTimeout(() => {
process.nextTick(() => console.log('nextTick'));
console.log('setTimeout2');
setImmediate(() => {
console.log('setImmediate2');
});
}, 0);
//結果1
// setImmediate1
// setTimeout2
// setTimeout1
// nextTick
// setImmediate2
// 結果2
// setTimeout2
// nextTick
// setImmediate1
// setImmediate2
// setTimeout1
複製代碼
JavaScript是單線程的,但Node自己實際上是多線程的,除了用戶代碼沒法並行執行外,全部的I/O請求是能夠並行執行的。 事件循環是Node異步I/O實現的核心,Node經過事件驅動的方式處理請求,使得其無須爲每一個請求建立額外的線程,省掉了建立和銷燬線程的開銷。同時也由於線程數較少,不受線程上下文切換的影響,維持了Node的高性能。 Node異步IO、非阻塞的特性,使它很是適用於IO密集、高併發的應用場景。
參考文章: