我來回答餓了么大前端的問題(2)

事件/異步

Promise

  • promise迷你書

    • Promise對象的三個狀態 has-resolution, has-rejection, unresolved
    • .then() 方法是異步調用的。
    var promise = new Promise(function (resolve){
           console.log("inner promise"); // 1
           resolve(42);
    });
    promise.then(function(value){
           console.log(value); // 3
    });
    console.log("outer promise"); // 2
    
    // 輸出
    inner promise // 1
    outer promise // 2
    42            // 3
    
  • 其實在我的理解當中,同步是相對的。對一串連續的事件使用promise封裝,他們之間是同步實行的,并不是對于外部。我們再來解釋下這個例子:

    setTimeout(function() {
      console.log(1)
    }, 0);
    new Promise(function executor(resolve) {
      console.log(2);
      for( var i=0 ; i<10000 ; i++ ) {
        i == 9999 && resolve();
      }
      console.log(3);
    }).then(function() {
      console.log(4);
    });
    console.log(5);
    
    // 輸出
    2
    3
    5
    4
    1
    

    這里會用到 Event Loop 的知識,后面有詳細說明。首先代碼順序執行,注冊了一個Timer事件,將Timer事件放入Timer隊列中。然后有一個promise被放入 poll 隊列中執行。先是有一個輸出2的同步事件,被放入 poll 隊列執行,再就出現了 resolve() ,一個異步事件先放入i/o callback隊列中,再有一個輸出3的同步事件,被放入 poll 隊列執行。再是將輸出5的同步事件放入 poll 執行,poll 空了以后,將i/o callback中的 resolve() 放入poll 執行,最后將Timer中的事件放入 poll 中執行。

Events

Events中的emit是同步的,會按照注冊順序來觸發監聽器。

一個事件監聽器中監聽同一個事件,會導致死循環?

const EventEmitter = require('events');

let emitter = new EventEmitter();

emitter.on('myEvent', () => {
  console.log('hi');
  emitter.emit('myEvent');
});

emitter.emit('myEvent');
  • 這種情況是會死循環的,其實就是無限的遞歸調用,運行一下果然崩棧了。 Maximum call stack size exceeded
const EventEmitter = require('events');

let emitter = new EventEmitter();

emitter.on('myEvent', function sth () {
  emitter.on('myEvent', sth);
  console.log('hi');
});

emitter.emit('myEvent');
  • 通過源碼解析 Node.js 中 events 模塊里的優化小細節
    • 這種情況不會出現死循環,因為在執行的過程當中是把原監聽數組拷貝一份出來執行監聽。當在監聽器中監聽同一個事件的時候,只會在原監聽數組當中添加監聽,而不會在這個拷貝后的數組添加。

Event Loop

  • JavaScript 運行機制詳解:再談Event Loop
    • 任務隊列

      • 所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
      • 主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
      • 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,于是結束等待狀態,進入執行棧,開始執行。
      • 主線程不斷重復上面的第三步。
      • 所以異步任務永遠是在同步任務之后開始執行的,不管他的代碼位置如何。
      • 排在任務隊列前面的事件優先進入執行棧,但是有些事件會被設置定時器,如果現在的時間小于了定時器的時間,那么會一直等到到達定時器的時間才會將事件加入執行棧。如果現在的時間大于了定時器的時間,也不會立即將它加入執行棧,會等到同步任務與它前面的異步任務執行完成以后再將它加入執行棧。
    • process.nextTick

      • 在當前"執行棧"的尾部,下一次Event Loop(主線程讀取"任務隊列")之前,觸發回調函數。也就是說,它指定的任務總是發生在所有異步任務之前。
      • 如果有多個 process.nextTick 語句(不管它們是否嵌套),將全部在當前"執行棧"執行。
    • setImmediate

      • 在當前"任務隊列"的尾部添加事件,也就是說,它指定的任務總是在下一次Event Loop時執行,這與 setTimeout(fn, 0) 很像。
      • process.nextTicksetImmediate 的一個重要區別:多個 process.nextTick 語句總是在當前"執行棧"一次執行完,多個setImmediate可能則需要多次loop才能執行完。事實上,這正是Node.js 10.0版添加 setImmediate 方法的原因,否則像下面這樣的遞歸調用 process.nextTick,將會沒完沒了,主線程根本不會去讀取"事件隊列"!
  • Node.js Event Loop 的理解 Timers,process.nextTick()
    • 這一篇對于 Event Loop 的分析就更加進階了,我們首先看 Event Loop 的各個階段。

         ┌───────────────────────┐
      ┌─>│        timers         │
      │  └──────────┬────────────┘
      │  ┌──────────┴────────────┐
      │  │     I/O callbacks     │
      │  └──────────┬────────────┘
      │  ┌──────────┴────────────┐
      │  │     idle, prepare     │
      │  └──────────┬────────────┘      ┌───────────────┐
      │  ┌──────────┴────────────┐      │   incoming:   │
      │  │         poll          │<─────┤  connections, │
      │  └──────────┬────────────┘      │   data, etc.  │
      │  ┌──────────┴────────────┐      └───────────────┘
      │  │        check          │
      │  └──────────┬────────────┘
      │  ┌──────────┴────────────┐
      └──┤    close callbacks    │
         └───────────────────────┘
      
      • timers: 執行各種定時器預約的操作, setTimeout(callback)setInterval(callback)
      • I/O callbacks: 執行除了 close 事件的callback和屬于 timerscallback以外的callback事件。
      • idle, prepare: 僅node內部使用。
      • poll: 獲取新的I/O事件, 適當的條件下node將阻塞在這里;(在我的理解中,這個階段才是一個 Event Loop 的開始)
      • check: 執行 setImmediate() 設定的callbacks;
      • close callbacks: close 事件的回調會在該階段執行。
      • 而還有一個獨立于 Event Loop 外的過程就是 process.nextTick() 它是在各個階段的切換階段進行調用。
    • Poll階段

      • 代碼中沒有設置 timer
        • poll quenue 不為空時,同步執行隊列中的所有事件。直到隊列為空,或執行的callback達到系統的上限。
        • poll quenue 為空時,如果設定了 setImmediate() 則進入 check階段,沒有設定的話,就阻塞等待callback進入隊列。
      • 代碼中設置 timer
        • poll queue進入空狀態時,event loop 將檢查timers,如果有1個或多個timers時間時間已經到達,event loop 將按循環順序進入timers階段,并執行timer queue.

進程

Process

  • process.cwd() :返回運行當前腳本的工作目錄的路徑。
  • process.chdir() :改變工作目錄。

process.nextTick

這個在上面已經分析過了,不贅述。

標準流

  • console.log 的實現

    exports.log = function() {
        process.stdout.write(format.apply(this, arguments) + '\n');
    };
    

    通過實現來說,我覺得 console.log 是同步的。

  • 同步輸入的實現 (nodeJS 中從命令行等待并讀入用戶輸入實現與用戶交互的方法):

    • 主要是思路是將 fs.readSync 的輸入流從定向到 process.stdin.fd
    • 我覺得還有一種方法是對輸入流進行監聽
  • Linux 中關于進程管理的命令

    • topTOP 是一個動態顯示過程,即可以通過用戶按鍵來不斷刷新當前狀態.如果在前臺執行該命令,它將獨占前臺,直到用戶終止該程序為止.比較準確的說, top 命令提供了實時的對系統處理器的狀態監視.它將顯示系統中CPU最“敏感”的任務列表.該命令可以按CPU使用.內存使用和執行時間對任務進行排序;而且該命令的很多特性都可以通過交互式命令或者在個人定制文件中進行設定.
    • psps 命令就是最基本同時也是非常強大的進程查看命令.使用該命令可以確定有哪些進程正在運行和運行的狀態、進程是否結束、進程有沒有僵尸、哪些進程占用了過多的資源等等。總之大部分信息都是可以通過執行該命令得到的。
    • pstree: Linux pstree命令將所有行程以樹狀圖顯示,樹狀圖將會以 pid (如果有指定) 或是以 init 這個基本行程為根 (root),如果有指定使用者 id,則樹狀圖會只顯示該使用者所擁有的行程。

Child Process

child_process.forkPOSIXfork 有什么區別?

  • POSIXfork每天進步一點點——論fork()函數與Linux中的多線程編程
    • 當程序調用 fork() 函數并返回成功之后,程序就將變成兩個進程,調用 fork() 者為父進程,后來生成者為子進程。這兩個進程將執行相同的程序文本,但卻各自擁有不同的棧段、數據段以及堆棧拷貝。子進程的棧、數據以及棧段開始時是父進程內存相應各部分的完全拷貝,因此它們互不影響。如果fork成功,子進程中fork的返回值是0,父進程中fork的返回值是子進程的進程號,如果fork不成功,父進程會返回錯誤。
  • 但是現在沒有找到 child_process.fork 如果不是對當前父進程的拷貝,那他的具體實現原理是什么。

child.killchild.send 的區別

  • 在談這兩者的區別的時候是需要先談一下 forkspawn 的區別:NODEJS硬實戰筆記(多進程)
    • 實例化一個 spawn 后,會返回一個 ChildProcess 對象,該對象中包含了 stdinstdoutstderr 流對象。而實例化一個 fork ,默認是會繼承父進程的stdinstdoutstderr 流(當然也可以打開),并且會單獨建立一個IPC通道,使得父子進程之間可以通過監聽 message 事件來進行通信,所以 fork 實際上是 spawn 的一個特例。
      • send 是需要依賴 IPC 通道進行通信的,所以只有通過 fork 的子進程才能與父進程之間使用 send 通信的。kill 是通過信號系統來進行通信,只要操作系統支持信號系統就行。

孤兒進程與僵尸進程

子進程的結束和父進程的運行是一個異步過程,即父進程永遠無法預測子進程到底什么時候結束。

  • 孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那么那些子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)所收養,并由init進程對它們完成狀態收集工作。 spawn 中可以通過 options.detached 指定父進程死亡后是否允許子進程存活。
    + 孤兒進程在被init進程接手以后,init進程會循環地 wait() 它已經退出的子進程,來進行善后,所以孤兒進程不會有什么危害。
  • 僵尸進程:一個進程使用 fork 創建子進程,如果子進程退出,而父進程并沒有調用 waitwaitpid 獲取子進程的狀態信息,那么子進程的進程描述符仍然保存在系統中。這種進程稱之為僵尸進程。
    + 僵尸進程因為沒有被父進程調用 wait ,進程號、退出狀態、運行時間等都被保存在內存中,特別是進程號一直被占用著,而系統的進程號又是有限的,所以大量的僵尸進程是會帶來非常大的威脅的。

Cluster

round-robin

其實就是找下一個空閑的 worker,但是我們可以看看 Cluster 的進化過程,從最開始的自由競爭(將引起 驚群效率),將每個連接放到 worker 競爭到由 master 獲取連接再進行分配。

進程間通信

Linux進程間通信之管道(pipe)、命名管道(FIFO)與信號(Signal)

  • 從原理上,管道利用fork機制建立,從而讓兩個進程可以連接到同一個PIPE上。最開始的時候,讀入流和輸出流都連接在同一個進程Process 1上。當fork復制進程的時候,會將這兩個連接也復制到新的進程(Process 2)。隨后,每個進程關閉自己不需要的一個連接 (一個關閉讀入流,一個關閉輸出流),這樣,剩下的連接就構成了PIPE。
  • 由于管道只能在父子進程之間進行通信,為了解決這一問題就提供了FIFO方法連接進程,FIFO (First in, First out)為一種特殊的文件類型,它在文件系統中有對應的路徑。當一個進程以讀(r)的方式打開該文件,而另一個進程以寫(w)的方式打開該文件,那么內核就會在這兩個進程之間建立管道。FIFO的好處在于我們可以通過文件的路徑來識別管道,從而讓沒有親緣關系的進程之間建立連接。
  • 信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是異步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什么時候到達。
    • 內核給一個進程發送軟中斷信號的方法,是在進程所在的進程表項的信號域設置對應于該信號的位。
    • 內核處理一個進程收到的信號的時機是在一個進程從內核態返回用戶態時。

守護進程

通過系統命令:Linux 守護進程的啟動方法

  • 首先要明確,通過使用 & 來啟動后臺任務的時候,實際上是不再繼承當前session的 stdin ,會繼續繼承 stdoutstderr
  • 然后需要明確當用戶退出當前session的時候,系統會向session發出 SIGHUP 信號,session再將該信號轉發給所有子進程,子進程收到后就會退出,所以普通的后臺進程是不能實現守護進程的需求的。
  • 那么實現的三個思路是:1. 不要讓session將 SIGHUP 信號轉發給后臺任務;2.將守護任務從后臺任務列表當中移出。但是光是這樣還是不行,因為后臺任務還是在繼承該session的 stdoutstderr,所以還需要將這兩個流重定向到外面。那么第三個思路是與前兩個截然不同的,第三個是重建session,將后臺任務啟動到這個新進程當中。

通過node:Nodejs編寫守護進程

  • 創建一個進程A。
  • 在進程A中創建進程B,我們可以使用fork方式,或者其他方法。
  • 對進程B執行 setsid 方法。
    • Linux進程組和會話
    • 該進程變成一個新會話的會話領導。
    • 該進程變成一個新進程組的組長。
    • 該進程沒有控制終端。
  • 進程A退出,進程B由init進程接管。此時進程B為守護進程。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 名稱 libev - 一個 C 編寫的功能全面的高性能事件循環。 概要 示例程序 關于 libev Libev 是...
    hanpfei閱讀 15,410評論 0 5
  • 弄懂js異步 講異步之前,我們必須掌握一個基礎知識-event-loop。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,750評論 0 5
  • 又來到了一個老生常談的問題,應用層軟件開發的程序員要不要了解和深入學習操作系統呢? 今天就這個問題開始,來談談操...
    tangsl閱讀 4,172評論 0 23
  • 研究生剛剛開學,于我而言是個嶄新的開始,不論過去發生了什么,我總應該拋開往事,昂首挺胸向前闊步走。 可我今天一整天...
    小念君閱讀 585評論 5 1
  • ? ? ? ?
    dabaofu閱讀 1,004評論 0 0