1. 程式人生 > >單執行緒與事件迴圈

單執行緒與事件迴圈

單執行緒指的是主執行緒是“單執行緒”的,所有阻塞的部分交給一個執行緒池處理,然後這個主執行緒通過一個佇列跟執行緒池協作,於是對我們寫到的js程式碼部分,不再關心執行緒問題,程式碼也主要由一堆callback回撥構成,然後主執行緒在不停的迴圈過程中,適時呼叫這些程式碼。

什麼是 Event Loop ?

單執行緒的 Node.js 能夠實現無阻塞IO的原因就是事件迴圈(Event Loop)。

現在大多數系統核心是多執行緒的,所以它們可以在後臺執行多個操作,當這些操作完成時,核心就會通知 Node.js,而這些操作的回撥函式被新增到事件輪詢列表(poll queue),並且 Node.js 會在適當的時機執行回撥函式。

當javascript程式碼執行的時候會將不同的變數存於記憶體中的不同位置:堆(heap)和棧(stack)中來加以區分。其中,堆裡存放著一些物件。而棧中則存放著一些基礎型別變數以及物件的指標。 但是我們這裡說的執行棧和上面這個棧的意義卻有些不同。

我們知道,當我們呼叫一個方法的時候,js會生成一個與這個方法對應的執行環境(context),又叫執行上下文。這個執行環境中存在著這個方法的私有作用域,上層作用域的指向,方法的引數,這個作用域中定義的變數以及這個作用域的this物件。 而當一系列方法被依次呼叫的時候,因為js是單執行緒的,同一時間只能執行一個方法,於是這些方法被排隊在一個單獨的地方。這個地方被稱為執行棧。

當一個指令碼第一次執行的時候,js引擎會解析這段程式碼,並將其中的同步程式碼按照執行順序加入執行棧中,然後從頭開始執行。如果當前執行的是一個方法,那麼js會向執行棧中新增這個方法的執行環境,然後進入這個執行環境繼續執行其中的程式碼。當這個執行環境中的程式碼 執行完畢並返回結果後,js會退出這個執行環境並吧這個執行環境銷燬。接著繼續執行佇列裡的下一段程式碼。

下面這個圖片非常直觀的展示了這個過程,其中的global就是初次執行指令碼時向執行棧中加入的程式碼:

從圖片可知,一個方法執行會向執行棧中加入這個方法的執行環境,在這個執行環境中還可以呼叫其他方法,甚至是自己,其結果不過是在執行棧中再新增一個執行環境。這個過程可以是無限進行下去的,除非發生了棧溢位,即超過了所能使用記憶體的最大值。

以上的過程說的都是同步程式碼的執行。那麼當一個非同步程式碼(如傳送ajax請求資料)執行後會如何呢?前文提過,js的另一大特點是非阻塞,實現這一點的關鍵在於下面要說的這項機制——事件佇列(Task Queue)。

js引擎遇到一個非同步事件後並不會一直等待其返回結果,而是會將這個事件掛起,繼續執行執行棧中的其他任務。當一個非同步事件返回結果後,js會將這個事件加入與當前執行棧不同的另一個佇列,我們稱之為事件佇列。被放入事件佇列不會立刻執行其回撥,而是等待當前執行棧中的所有任務都執行完畢, 主執行緒處於閒置狀態時,主執行緒會去查詢事件佇列是否有任務。如果有,那麼主執行緒會從中取出排在第一位的事件,並把這個事件對應的回撥放入執行棧中,然後執行其中的同步程式碼...,如此反覆,這樣就形成了一個無限的迴圈。這就是這個過程被稱為“事件迴圈(Event Loop)”的原因。

       擁有非同步I/O的node為什麼可以支援高併發呢?

因為I/O操作是由node的工作執行緒去執行的(nodejs底層的libuv是多執行緒的執行緒池用來並行io操作),且主執行緒是不需要等待結果返回的,只要發出指令馬上就可以去忙其他事情了。   

  • 同步任務都在主執行緒上執行,形成一個執行棧
  • 主執行緒之外,還存在一個任務佇列。只要非同步任務有了執行結果,就在任務佇列之中放置一個事件。
  • 一旦執行棧中的所有同步任務執行完畢,系統就會讀取任務佇列,將佇列中的事件放到執行棧中依次執行
  • 主執行緒從任務佇列中讀取事件,這個過程是迴圈不斷的