2017-04-18 95 views
2

假設makeBurger()將需要10秒單線程同步和異步混亂

在同步程序中,

function serveBurger() { 
    makeBurger(); 
    makeBurger(); 
    console.log("READY") // Assume takes 5 seconds to log. 
} 

這將需要總共25秒來執行。

因此,對於NodeJs可以說我們做了一個異步版本makeBurgerAsync()這也需要10秒鐘。

function serveBurger() { 
    makeBurgerAsync(function(count) { 

    }); 
    makeBurgerAsync(function(count) { 

    }); 
    console.log("READY") // Assume takes 5 seconds to log. 
} 

由於它是單線程。我很困惑想象背後的真實情況。

  1. 因此,當函數運行時,兩個異步函數都將進入事件循環,並且將立即執行console.log("READY")
  2. 雖然console.log("READY")正在執行,但是兩個異步函數都沒有完成任何工作嗎?由於單線程佔用console.log 5秒鐘。
  3. 完成console.log後。 CPU將有時間在兩個異步之間進行切換,以便每次都可以運行一些函數。

所以根據這個,函數不一定會導致更快的執行,由於事件循環之間的切換,異步可能會更慢?我想,在一天結束的時候,所有東西都會傳播到一個單獨的線程上,這個線程與同步版本是一樣的。

我可能錯過了一些非常大的概念,所以請讓我知道。謝謝。

編輯 這是有道理的,如果異步操作都像查詢數據庫等,基本上將的NodeJS只是說「嘿DB處理這對我,而我會做別的事情。」然而,我不明白的情況是nodejs自身內部的自定義回調函數。

EDIT2

function makeBurger() { 
    var count = 0; 
    count++; // 1 time 
    ... 
    count++; // 999999 times 
    return count; 
} 

function makeBurgerAsync(callback) { 
    var count = 0; 
    count++; // 1 time 
    ... 
    count++; // 999999 times 
    callback(count); 
} 
+0

這取決於makeBurgerAsync是否更像setTimeout,它推遲了事件循環中的執行,或者更像是等待某些事件使得只有在實際方法完成後才執行連接。在後一種情況下,console.log不會在5秒內完成,在兩個調用完成後它將在那裏。 –

+1

它取決於*爲什麼*需要10秒才能完成「異步」操作。如果阻塞事件循環的時間是10秒,那麼它會阻塞10秒,但它只會在* console.log發生後阻塞*,因爲需要10秒的操作將被推送到回調函數中隊列。 (並且它將阻止在該10秒窗口期間進入的任何請求)如果10秒鐘等待在例如文件i/o或http等,因此不阻塞事件循環,則沒有明顯的阻塞。 –

+0

@WiktorZychla「」更像是等待一些讓實際方法完成後連續執行的東西。「你能澄清一下這句話嗎?謝謝你的迴應! – Zanko

回答

7

在node.js中,所有異步操作完成的Node.js的Javascript單一線程之外他們的任務。它們要麼使用本地代碼線程(如node.js中的磁盤I/O),要麼根本不使用線程(例如事件驅動的網絡或定時器)。

你不能把一個完全用node.js寫成的同步操作變成異步。異步操作是異步的,因爲它調用了一些在本地代碼中實現的函數,並以實際上是異步的方式編寫。因此,爲了實現異步,必須專門編寫它以使用低級別的操作,這些操作本身與異步本地代碼實現異步。

這些帶外操作,然後通過事件隊列與主要node.js Javascript線程進行通信。當其中一個異步操作完成時,它將一個事件添加到Javascript事件隊列中,然後當單個node.js線程完成當前正在執行的任務時,它會從事件隊列中抓取下一個事件並調用與該事件關聯的回調。

因此,您可以有多個並行運行的異步操作。並行運行3個操作的端到端運行時間通常比按順序運行相同的3個操作要短。

讓我們來看看一個真實的異步的情況,而不是你的僞代碼:

function doSomething() { 
    fs.readFile(fname, function(err, data) { 
     console.log("file read"); 
    }); 
    setTimeout(function() { 
     console.log("timer fired"); 
    }, 100); 

    http.get(someUrl, function(err, response, body) { 
     console.log("http get finished"); 
    }); 

    console.log("READY"); 
} 

doSomething(); 

console.log("AFTER"); 

這裏發生了什麼一步一步:

  1. fs.readFile()啓動。由於node.js使用線程池實現文件I/O,因此此操作將傳遞給node.js中的線程,並且將在單獨的線程中運行。
  2. 無需等待fs.readFile()完成,setTimeout()被調用。這在libuv中使用了一個定時器子系統(node.js構建的跨平臺庫)。這也是非阻塞的,所以定時器被註冊,然後繼續執行。
  3. http.get()被調用。這將發送所需的http請求,然後立即返回到進一步執行。
  4. console.log("READY")將運行。
  5. 這三個異步操作將以不確定的順序完成(無論哪個操作先完成它的操作都將首先完成)。爲了討論的目的,我們假設setTimeout()首先完成。當它完成時,node.js中的一些內部事件將使用定時器事件和註冊回調在事件隊列中插入一個事件。當node.js主JS線程完成其他任何JS時,它將從事件隊列中獲取下一個事件並調用與其關聯的回調。
  6. 爲了說明的目的,我們假設當該定時器回調正在執行時,fs.readFile()操作完成。使用它自己的線程,它會在node.js事件隊列中插入一個事件。
  7. 現在setTimeout()回調完成。那時,JS解釋器會檢查事件隊列中是否有其他事件。 fs.readfile()事件處於隊列中,因此它抓住並調用與之相關的回調。該回調執行並結束。
  8. 一段時間後,http.get()操作結束。在node.js的內部,事件被添加到事件隊列中。由於事件隊列中沒有其他東西,並且JS解釋器當前沒有執行,所以該事件可以立即得到服務,並且可以調用http.get()的回調。

每以上事件序列中,你會看到這個控制檯:

READY 
AFTER 
timer fired 
file read 
http get finished 

請記住,這裏的最後三個行的順序是不確定的(它只是基於不可預知的執行速度),所以這裏的精確命令只是一個例子。如果您需要按特定順序執行這些操作,或者需要知道三者何時完成,那麼您必須添加額外的代碼才能跟蹤該操作。


由於看起來您正試圖通過使異步的代碼運行得更快,所以我要重複一遍。你不能採用完全用Javascript編寫的同步操作並「使其異步」。您必須從頭開始重新編寫以使用根本不同的異步低級操作,否則您必須將其傳遞給其他進程以執行,然後在完成時收到通知(使用工作進程或外部進程或本機代碼插件或類似的東西)。

+0

這解決了我的困惑!感謝你這樣一個詳細的職位。你不能採用完全用Javascript編寫的同步操作並「使其異步」。但是,我可以在同步函數上設置setTimeout,使其有機會進入事件循環。但它沒有什麼好處。我猜測異步與其他資源進行交流並不會影響線程。 – Zanko

+0

您提到「fs.readFile()」已啓動,由於node.js使用線程池實現文件I/O,因此此操作將傳遞給node.js中的一個線程,並將在單獨的線程中運行。這讓我感覺像node.js間接使用多個線程? – Zanko

+0

@Zanko - 當人們在node.js的內容中說「單線程」時,他們正在談論JS解釋器。,您的JS代碼只運行在單個線程中,並且兩段JavaScript之間永遠不會有線程衝突。有一些使用本地代碼的庫函數(其中'fs.readFile()'是一個例子),它們使用線程來實現異步功能。這仍然不能使JS解釋器以任何方式進行多線程。異步函數通過事件隊列與JS解釋器的單線程進行通信和同步。 – jfriend00