Event Loop

Wai Lin Kyaw
4 min readJan 20, 2021

JavaScript မှာ asynchronous ပိုင်းကို ကောင်းကောင်း handle လုပ်နိုင်ဖို့ဆိုရင် Event Loop အကြောင်း နားလည်ထားဖို့ လိုပါတယ်။ သိထားရင် ဘယ်နေရာတွေမှာ အသုံးဝင်မလဲ ဆိုတော့ async code တွေ နောက်ကွယ်မှာ ဘယ်လိုအလုပ်လုပ်လဲ၊ performance optimization တွေ ဘယ်လိုလုပ်မလဲ ဆိုတာတွေ၊ နောက် browser ထဲမှာဆိုရင် UI rendering ကို block ဖြစ်တာတွေ လျှော့ချနိုင်မယ်၊ တခြားဟာတွေ လည်းရှိသေးတယ်။

Web page တွေမှာ scroll လုပ်တာတို့၊ button click တာတို့ စာသားတွေ select မှတ်တာတို့ ကိုကောင်းကောင်း ဖော်ပြနိုင်အောင် ပြောချင်တာက user interaction ပေါ်မူတည်ပြီး screen ပေါ်မှာ ကောင်းကောင်း မြင်နိုင်ဖို့ browser တွေက web pages တွေကို 1 second ကို အကြိမ် 60 လောက် update လုပ်ရတယ်။ ( 60 fps ) လို့သိကြမှာပါ။ ဒီတော့ Event Loop ကိုကောင်းကောင်း နားမလည် ထားရင် rendering ကို block မဖြစ်အောင် ဘယ်လိုရေးရမလဲ ဆိုတာတောင် သိမှာ မဟုတ်ဘူး။

ပထမဆုံး JavaScript က single-threaded ဖြစ်တယ်ဆိုတာ သိရမယ်။ ဆိုလိုတာက JS ကနေ တစ်ကြိမ်မှာ တစ်ခုပဲ run လို့ရတယ်။ service worker တို့ web worker တို့ နောက် remote csp လိုကောင်မျိုးတွေ သုံးပြီး background task တွေခိုင်းလို့ မရဘူးလို့ ပြောနေတာမဟုတ်ပါဘူး။ JS engine ထဲမှာ run တာက single-threaded အနေနဲ့ပဲ run လို့ရတာပါ။ တစ်နေရာရာမှာ block ဖြစ်နေပြီဆိုတာနဲ့ ကျန်တာတွေက ဘာမှလုပ်လို့ မရပါဘူး။

Call Stack demo

နောက်တစ်ခုက Call Stack ပါ။ JS ကနေ global execution context နဲ့ function execution context တွေကို track လိုက်ဖို့ Call Stack ကိုသုံးတယ်။ main program ကို စ run ပြီဆိုတာနဲ့ Call Stack ပေါ်ကို global execution context တင်ပါတယ်။

function call တစ်ခုခု တွေ့ရင် Call Stack ပေါ်ကို သက်ဆိုင်ရာ function execution context ကိုထပ်တင်တယ်။ ပုံမှာ ကြည့်ကြည့်ပါ။ global မှာ foo ဆိုတဲ့ function ကိုခေါ်တော့ foo ကို Call Stack ပေါ်တင်တယ်၊ နောက် foo ကနေ bar ဆိုတဲ့ function ကို ထပ်ခေါ်တော့ bar ကို Call Stack ပေါ်ထပ်တင်မယ်။ bar ထဲမှာ လုပ်စရာရှိတာ လုပ်ပြီး သွားရင် Call Stack ပေါ်ကနေ pop လုပ်ချမယ်ပ။ ဒီတော့ အကုန် run လို့ ပြီးသွားရင် Call Stack က empty ဖြစ်သွားမယ်။

setTimeout( function log() {

console.log(“timeout!”);

}, 0);

console.log(“Hey!”);

ဒီအပေါ်က code ကို run ကြည့်ရင် Hey! ကအရင်လာပြီး timeout! ကနောက်မှ ရလာတာ တွေ့ရမယ်။ အကြောင်းက setTimeout က asynchronous code ဖြစ်ပြီးတော့ browser ကနေ ပေးထားတဲ့ API တစ်ခုပဲ။ ဒီတော့ setTimeout ကို callback function တစ်ခုနဲ့ delay ပေးပြီး timer schedule လုပ်တဲ့ အခါမှာ browser က ပေးလိုက်တဲ့ delay ပြီးမှ run မှာတော့သေချာတယ်။ ဒါပေမဲ့ ပိုကြာချင်လည်း ကြာမယ်ပေါ့။ delay ထက် ပိုမမြန်ဘူးဆိုတာတော့ သေချာတယ်။

JS မှာ timer နဲ့ schedule ကမပါဘူး။ browser ကပေးထားတဲ့ API ဆိုတော့ JS ကနေ ခေါ်သုံး၊ လိုအပ်တာကို browser ကနေလုပ်ပေးမယ်၊ ပြီးရင် JS ကိုပြောမယ် ပြီးပြီ၊ callback ကို run လို့ရပြီ။ ပြသနာရှိတာက သူပြီးသွားတဲ့ အချိန်မှာ Call Stack ပေါ်မှာ တစ်ခုခုရှိနေမယ် ဆိုရင် call stack ပေါ်တင်လို့မရဘူး။ ဒီတော့ Call Stack empty ဖြစ်အောင် စောင့်ရတယ်။ synchronous code တွေ အကုန် run ပြီးသွားအောင်စောင့်။ ပြီးမှ ခုနက setTimeout က callback ကို run မယ်ပေါ့။ အဲ့တော့ စောင့်နေတဲ့ အချိန် တခြား callback တွေ ထပ်ရောက်လာရင်ရော ဘယ်လို လုပ်မလဲဆိုတဲ့ ပြသနာ ထပ်ရှိလာတယ်။

ဒီတော့ callbacks တွေကို queue တစ်ခုတည်း ထည့်ထားလိုက်မယ်။ synchronous code တွေ run လို့ပြီးသွားတာနဲ့ queue ထဲက ကောင်တွေကို တစ်ခုချင်း Call Stack ပေါ်ဆွဲတင်ပြီး ပြန် run လို့ရသွားပြီ။ ဒီနေရာမှာ empty ဖြစ်သွားရင် run ဖို့စောင့်နေတဲ့ pending callback functions တွေ queue ထဲမှာ ရှိလား၊ ရှိတယ်ဆိုရင် Call Stack က empty ဖြစ်သွားပြီလား။ Call Stack ပေါ်မှာ တစ်ခုခု ရှိနေသေးတာလား စသည်ဖြင့် အဆက်မပြတ် အမြဲတမ်း check လုပ်ပေးနေတဲ့ ကောင်ကို Event Loop လို့ခေါ်တာ။ Loop iteration တစ်ခုတိုင်းကို tick လို့ခေါ်တယ်။ Call Stack က empty ဖြစ်သွားရင် queue ထဲက ရှေ့ဆုံးက ကောင်ကို Call Stack ပေါ် တင်ပေးပြီး run မယ်။ next tick ကျရင် နောက်တစ်ခုပေါ့။ Event Loop က အမြဲတမ်း run နေတာ၊ infinite loop လိုပဲ။ ဒါပေမဲ့ CPU efficient ဖြစ်အောင်လည်း design လုပ်ထားတာ။

Callback functions တွေကို ဘယ်နေရာက လာတဲ့ callback လဲပေါ် မူတည်ပြီး queue တွေခွဲပစ်လိုက်တယ်။ နောက် Event Loop ကနေ priority အလိုက် ဘယ် queue ထဲကကောင်ကို အရင် Call Stack ပေါ်တင်မလဲ ဆိုတာ ကြည့်တယ်။ setTimeout လို browser API တွေကလာရင် macrotask queue ထဲကို ထည့်တယ်။ macrotask queue ဆိုတာ chrome အခေါ်ပေါ့။ host environment ပေါ်မူတည်ပြီး နာမည် ကွဲတာတော့ရှိမယ်။ macrotask queue ကိုပဲ callback queue လို့လည်း ခေါ်ကြသေးတယ်။ ES6 မှာပါလာတဲ့ Promises တွေအတွက်ကျတော့ microtask queue ကိုသုံးတယ်။ ECMA spec မှာတော့ Job queue လို့ခေါ်တယ်။ microtask queue က macro task queue ထက် priority ပိုမြင့်တယ်။

ဥပမာ ပြောရရင် setTimeout အရင် timer ပြည့်သွားလို့ marotask queue ထဲဝင်လာမယ်။ နောက် ခဏနေမှ Promise တစ်ခုခု resolve/reject ဖြစ်ပြီး microtask queue ထဲဝင်လာမယ် နှစ်ခုလုံးက Call Stack empty မဖြစ်သေးလို့ စောင့်နေတယ် ဆိုပါတော့။ Call Stack empty ဖြစ်လို့ Event loop ကနေ ဆွဲတင်တဲ့ အချိန်ကျရင် microtask queue ထဲကောင်ကို အရင်ယူမှာ။ ဒါကို ပြောချင်တာ။

NodeJS မှာဆို process.nextTick က microtask ထက် priority ပိုမြင့်တယ်။ nextTick ဆိုတဲ့ အတိုင်း Event Loop ကနေ next iteration စတာနဲ့ ဒီကောင်က အလုပ် လုပ်တယ်။ Promise ကနေ မဟုတ်ဘဲ microtask queue ထဲကို ထည့်ချင်ရင် queueMicrotask ဆိုတဲ့ API တစ်ခုရှိတယ်။ နောက် setImmediate ဆိုတဲ့ API တစ်ခုရှိတယ် သူကလည်း schedule ချတာပဲ။ ဒါပေမဲ့ macrotask queue ထက် priority နိမ့်တယ်။ ES6 မတိုင်ခင် native promises တွေမရှိခင် နာမည်ကြီးခဲ့တဲ့ promise library တစ်ချို့ ( ဥပမာ bluebird ) တို့မှာ schedule ချဖို့ အတွက် ဒီ setImmediate ကိုသုံးတာ။ အခုထိတစ်ချို့သုံးနေတာ ရှိတယ်။ mongoose ရဲ့ အရင် version တွေမှာ bluebird Promise တွေသုံးထားတော့ သတိထားလို့ရအောင်ပါ။ promise library တွေက duck typing သုံးပြီး thenable ရအောင် ပေးထားပေမဲ့ implementation ပေါ်မူတည်ပြီး အလုပ်လုပ်ပုံကွာမှာပါ။ nextTick မှာလည်း Node.JS version ပေါ်မူတည်ပြီးကွဲတာ ရှိတယ်။ Node.JS မှာက I/O တွေပါတော့ browser မှာထက် ပိုတာတွေရှိတယ်၊ browser မဟုတ်တော့ rendering နဲ့ ပက်သက်တာ မပါတော့ဘူးပေါ့။

microtask က queues တွေထဲမှာ priority အမြင့်ဆုံးပဲ။ နောက်တစ်ချက်က တခြား queue တွေမှာ iteration တစ်ကြိမ်မှာ callback တစ်ခုစီကိုပဲ enqueue လုပ်ပြီး Call Stack ပေါ်တင်မှာ။ microtask queue ထဲကကောင်တွေကျတော့ အကုန်လုံးကို တစ်ခုပြီးတစ်ခု queue empty မဖြစ်မချင်း Call Stack ပေါ်တင် run နေမှာ။ microtask ထဲကို ထပ်ထည့်ရင်လည်း တခြား queue တွေကို အလှည့်မပေးပဲ သူ့ထဲရှိတာ မကုန်မချင်း အကုန် run ခိုင်းနေမှာ။ microtask ထဲက task တစ်ခုကနေ နောက်ထပ် task တွေ ထပ်ထည့်တာလည်း ဖြစ်နိုင်တာပဲလေ။ ဒီလိုပုံစံမျိုးကျ ဘာနဲ့သွားတူလဲဆိုတော့ တန်းစီပြီး စောင့်နေရင်း ကိုယ့်အလှည့်ပြီးရင် နောက်ဆုံးကနေ ပြန်စီရမှာမျိုး မလုပ်ပဲ ကြားထဲက ဖြတ်ဝင်တာမျိုးပေါ့။ အဲ့လို ထပ်ထပ် ထည့်‌နေတာ က တစ်ချို့ အခြေနေမှာ ဘာပြသနာရှိလဲ ဆိုတော့ microtask queue က block ထားသလိုဖြစ်လာမယ်။ Event Loop ကနေ တခြား queue တွေကို အလှည့်ပေးလို့ကိုမရတော့ဘူးပေါ့။ Concurrency model မှာ starving လို့ခေါ်တယ်။

microtask ကိုသုံးတဲ့ ကောင်တွေမှာ Promise ရှိမယ်၊ နောက် ES8 က async/await တွေကိုလည်း microtask ကိုသုံးတာပဲ။ JS မှာ generator တွေသုံးပြီး CSP Channel ဆောက်တဲ့ သူတွေဆိုရင် ဒီ starving ပြသနာကို ကောင်းကောင်း ကြုံဖူးလိမ့်မယ်။ 2018 လောက်က async/await ကို ECMA မှာ official land မလုပ်ခင် starving ဖြစ်စေနိုင်တာကို အရင်ပြင်ဖို့ github မှာ Node ကိုရော tc39 ကိုရော တော်တော် ပြောကြတာ။ ဒါပေမဲ့လည်း ပြောတာပဲ ရှိပါတယ်၊ ES8 မှာ async/await ပါလာတယ်။ starving ပြသနာက လည်း ခုထိမပြင်ကြဘူး ဒီတိုင်းပဲ။ Programmer တွေဖက်ကနေပဲ starving မဖြစ်အောင် သတိထားပြီး ရေးဆိုတဲ့ သဘောပါ။

UI rendering နဲ့ Node ကနေ I/O တွေကို handle လုပ်ပုံ အကြောင်းကတော့ သပ်သပ်ရေးမှ အဆင်ပြေမှာပါ။ Single-threaded JavaScript မှာ asynchronous code တွေကို ကောင်းကောင်း နားလည်ဖို့၊ Node မှာဆို Non-Blocking I/O တွေရေးနိုင်ဖို့ ဆိုရင်တော့ Event Loop ကိုကောင်းကောင်း သိထား သင့်ပါတယ်။

--

--