1. 程式人生 > 其它 >面試官:Redis 分散式鎖如何自動續期

面試官:Redis 分散式鎖如何自動續期

程序與執行緒 -- 涉及⾯試題:程序與執行緒區別? JS 單執行緒帶來的好處?

  • JS 是單執行緒執⾏的,但是你是否疑惑過什麼是執行緒?
  • 講到執行緒,那麼肯定也得說⼀下程序。本質上來說,兩個名詞都是 CPU ⼯作時間⽚的⼀個描述。
  • 程序描述了 CPU 在運⾏指令及載入和儲存上下⽂所需的時間,放在應⽤上來說就代表了⼀個程式。
  • 執行緒是程序中的更⼩單位,描述了執⾏⼀段指令所需的時間。
    --- 把這些概念拿到瀏覽器中來說,當你開啟⼀個 Tab ⻚時,其實就是建立了⼀個程序,⼀個程序中可以有多個執行緒,⽐如渲染執行緒、 JS 引擎執行緒、HTTP 請求執行緒等等。當你發起⼀個請求時,其實就是建立了⼀個執行緒,當請求結束後,該執行緒可能就會被銷燬。
  • 上⽂說到了 JS 引擎執行緒和渲染執行緒,⼤家應該都知道,在 JS 運⾏的時候可能會阻⽌ UI 渲染,這說明了兩個執行緒是互斥的。這其中的原因是因為 JS 可以修改 DOM ,如果在 JS 執⾏的時候 UI 執行緒還在⼯作,就可能導致不能安全的渲染 UI 。這其實也是⼀個單執行緒的好處,得益於 JS 是單執行緒運⾏的,可以達到節省記憶體,節約上下⽂切換時間,沒有鎖的問題的好處。

執⾏棧 -- 涉及⾯試題:什麼是執⾏棧?
* 可以把執⾏棧認為是⼀個儲存函式調⽤的棧結構,遵循先進後出的原則。
當開始執⾏ JS 程式碼時,⾸先會執⾏⼀個 main 函式,然後執⾏我們的程式碼。
根據先進後出的原則,後執⾏的函式會先彈出棧,在圖中我們也可以發現, foo 函式後執⾏,當執⾏完畢後就從棧中彈出了。

                    function foo() {
                       throw new Error('error')
                    }
                    function bar() {
                       foo()
                    }
                    bar()
  ⼤家可以在上圖清晰的看到報錯在 foo 函式, foo 函式⼜是在 bar 函式 中調⽤的。

當我們使⽤遞迴的時候,因為棧可存放的函式是有限制的,⼀旦存放了過多的函式且沒有得到釋放的話,就會出現爆棧的問題。

                    function bar() {
                       bar()
                    }
                    bar()

瀏覽器中的 Event Loop
涉及⾯試題:非同步程式碼執⾏順序?解釋⼀下什麼是 Event Loop ?

 * 眾所周知 JS 是⻔⾮阻塞單執行緒語⾔,因為在最初 JS 就是為了和瀏覽器互動⽽誕⽣的。
 * 如果 JS 是⻔多執行緒的語⾔話,我們在多個執行緒中處理 DOM 就可能會發⽣問題(⼀個執行緒中新加節點,另⼀個執行緒中刪除節點)。
  • JS 在執⾏的過程中會產⽣執⾏環境,這些執⾏環境會被順序的加⼊到執⾏棧中。如果遇到非同步的程式碼,會被掛起並加⼊到 Task (有多種 task ) 佇列中。⼀旦執⾏棧為空,Event Loop 就會從 Task 佇列中拿出需要執⾏的程式碼並放⼊執⾏棧中執⾏,所以本質上來說 JS 中的非同步還是同步⾏為。

                  console.log('script start');
    
                  setTimeout(function() {
                     console.log('setTimeout');
                  }, 0);
    
                  console.log('script end');
    
    • 不同的任務源會被分配到不同的 Task 佇列中,任務源可以分為 微任務 ( microtask ) 和 巨集任務( macrotask )。

    • 在 ES6 規範中,microtask 稱為 jobs , macrotask 稱為 task 。

        console.log('script start');
        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');
        // script start => Promise => script end => promise1 => promise2 => setTimeout
      

      --- 以上程式碼雖然 setTimeout 寫在 Promise 之前,但是因為 Promise 屬於 微任務 而 setTimeout 屬於巨集任務。

微任務

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

巨集任務

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

-- 巨集任務中包括了 script ,瀏覽器會先執⾏⼀個巨集任務,接下來有 非同步程式碼 的話就先執⾏微任務。

所以正確的⼀次 Event loop 順序是這樣的

  • 執⾏同步程式碼,這屬於巨集任務;
  • 執⾏棧為空,查詢是否有微任務需要執⾏;
  • 執⾏所有微任務 ;
  • 必要的話渲染 UI ;
  • 然後開始下⼀輪 Event loop ,執⾏巨集任務中的非同步程式碼。

--- 通過上述的 Event loop 順序可知,如果巨集任務中的非同步程式碼有⼤量的計算 並且需要操作 DOM 的話,為了更快的響應界⾯響應,我們可以把操作 DOM 放⼊微任務中。

Node 中的 Event loop

  • Node 中的 Event loop 和瀏覽器中的不相同。

  • Node 的 Event loop 分為 6 個階段,它們會按照順序反覆運⾏。

    ┌───────────────────────┐
    ┌─> │ timers │
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    │ │ I/O callbacks │
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    │ │ idle, prepare │
    │ └──────────┬────────────┘ ┌───────────────┐
    │ ┌──────────┴────────────┐ │ incoming: │
    │ │ poll │<──connections─── │
    │ └──────────┬────────────┘ │ data, etc. │
    │ ┌──────────┴────────────┐ └───────────────┘
    │ │ check │
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    └─ ─┤ close callbacks │
    └───────────────────────┘

timer
timers 階段會執⾏ setTimeout 和 setInterval
⼀個 timer 指定的時間並不是準確時間,⽽是在達到這個時間後儘快執⾏回撥,可能會因 為系統正在執⾏別的事務⽽延遲 I/O
I/O 階段會執⾏除了 close 事件,定時器和 setImmediate 的回撥 poll
poll 階段很重要,這⼀階段中,系統會做兩件事情 執⾏到點的定時器 執⾏ poll 佇列中的事件
並且當 poll 中沒有定時器的情況下,會發現以下兩件事情 如果 poll 佇列不為空,會遍歷回撥佇列並同步執⾏,直到佇列為空或者系統限制 如果 poll 佇列為空,會有兩件事發⽣ 如果有 setImmediate 需要執⾏, poll 階段會停⽌並且進⼊到 check 階段執⾏
setImmediate
如果沒有 setImmediate 需要執⾏,會等待回撥被加⼊到佇列中並⽴即執⾏回撥 如果有別的定時器需要被執⾏,會回到 timer 階段執⾏回撥。 check
check 階段執⾏ setImmediate
close callbacks
close callbacks 階段執⾏ close 事件
並且在 Node 中,有些情況下的定時器執⾏順序是隨機的
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
setTimeout(() => {
console.log('setTimeout'); }, 0);
setImmediate(() => {
console.log('setImmediate');
})
// 這⾥可能會輸出 setTimeout,setImmediate
// 可能也會相反的輸出,這取決於效能
// 因為可能進⼊ event loop ⽤了不到 1 毫秒,這時候會執⾏ setImmediate
// 否則會執⾏ setTimeout

上⾯介紹的都是 macrotask 的執⾏情況, microtask 會在以上每個階段完 成後⽴即執⾏

setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
}) }, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
}) }, 0)
// 以上程式碼在瀏覽器和 node 中列印情況是不同的
// 瀏覽器中⼀定列印 timer1, promise1, timer2, promise2
// node 中可能列印 timer1, timer2, promise1, promise2
// 也可能列印 timer1, promise1, timer2, promise2

Node 中的 process.nextTick 會先於其他 microtask 執⾏

setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1"); }); }, 0);
process.nextTick(() => {
console.log("nextTick"); });
// nextTick, timer1, promise1

對於 microtask 來說,它會在以上每個階段完成前清空 microtask 隊 列,下圖中的 Tick 就代表了 microtask