Learn JavaScript

lesson ১ / ৯ · async 1: callback আর event loop

Module ১২ · async 1: callback আর event loop

Event loop-এর ছবি: একটা thread, একটা stack আর দুটো queue

Freeপড়া

এই lesson-এ যা শিখবে

  • বুঝিয়ে বলো setTimeout-কে দেওয়া callback কেন চলতি code শেষ হওয়ার পরেই চলে।
  • মাথায় ছবিটা আঁকো: call stack, host-এর timer, task queue আর microtask queue একসাথে কীভাবে কাজ করে।
  • সাধারণ log, timer আর microtask মেশানো একটা ছোট program কোন ক্রমে print করবে, আগেই বলে দাও।

Rafi একটা quiz app বানাচ্ছে। Student "Submit" চাপলে ও চায় সাথে সাথে একটা message আসুক, তারপর নম্বর দেওয়া শুরু হোক। তাই ও লেখে setTimeout(showMessage, 0), ভাবে শূন্য মানে "এখনই"। তারপর নম্বর দেওয়ার loop চলে, আর message আসে সেটা শেষ হওয়ার পরে। শূন্য millisecond-এর timer পুরো সময়টা বসে ছিল।

Rafi-র timer নষ্ট হয়নি। JavaScript যা কথা দেয়, timer ঠিক তাই করেছে। একটা timer-এর callback চলতে থাকা code-এর মাঝে কখনো ঢুকে পড়ে না। Stack খালি হওয়া পর্যন্ত সে অপেক্ষা করে, তারপর event loop তাকে চালাতে দেয়। এই module সেই loop নিয়ে। একবার ছবিটা চোখে ভাসলে যেকোনো async program-এর print-এর ক্রম আর অবাক করবে না।

একটা thread, একটা call stack

JavaScript তোমার code চালায় একটাই thread-এ। Thread মানে কাজের একটা লাইন: একটা কাজ করে, তারপর পরেরটা। তোমার program-এ দ্বিতীয় কোনো কর্মী নেই, যে তোমার loop ব্যস্ত থাকার সময় timer-এর callback চালিয়ে দেবে।

ওই একটা thread একটা call stack রাখে। Module 5-এ দেখেছ: প্রতিটা function call একটা frame চাপায়, প্রতিটা return একটা frame নামায়। Main script নিজেই সবচেয়ে নিচের frame। Stack-এ কিছু থাকা মানেই thread ব্যস্ত।

const start = Date.now();
setTimeout(() => {
  const waited = Date.now() - start;
  console.log("timer ran, waited 200 ms or more:", waited >= 200);
}, 0);
let spins = 0;
while (Date.now() - start < 200) spins++;
console.log("loop done");
loop done
timer ran, waited 200 ms or more: true
Run in Compiler

Timer চেয়েছিল 0 ms। কিন্তু loop 200 ms ধরে stack আটকে রাখল, তাই আগে print হলো "loop done"। তারপরেই timer-এর callback চলল, আর দ্বিতীয় লাইনটা বলছে সে পুরো 200 ms অপেক্ষা করেছে।

অপেক্ষার কাজটা করে host

JavaScript-এর thread যদি একটাই হয়, millisecond গোনে কে? তোমার code না। গোনে host: যে program JavaScript চালায়। Browser-এ host হলো browser নিজে; আমাদের Playground আর server-এ host হলো Node.js।

Host কিছু API দেয়, যেগুলো কোনো কিছুর জন্য অপেক্ষা করে: timer, file পড়া, network-এর উত্তর, click। Browser-এ এদের বলে Web APIs। Node-এর নিজের আলাদা সেট আছে। তুমি setTimeout(cb, 100) ডাকলে cb-কে host-এর হাতে তুলে দাও, আর নিয়ন্ত্রণ সাথে সাথে ফিরে পাও। setTimeout নিজে প্রায় কোনো সময়ই নেয় না।

console.log("before");
const id = setTimeout(() => console.log("callback"), 100);
console.log("setTimeout returned, the timer id is an object:", typeof id === "object");
console.log("after");
before
setTimeout returned, the timer id is an object: true
after
callback
Run in Compiler

setTimeout সাথে সাথে একটা timer id দিয়ে ফিরে আসে, আর script চলতে থাকে। Node-এ ওই id একটা object; browser-এ একটা সংখ্যা। Lesson 02-এ এই id দিয়ে timer বাতিল করা দেখবে।

Task queue

Host-এর timer শেষ হলেও callback সাথে সাথে চলতে পারে না, কারণ stack তখন ব্যস্ত থাকতে পারে। তাই সে একটা লাইনে দাঁড়ায়: task queue, যাকে macrotask queue বা callback queue-ও বলে। Timer, শেষ হওয়া file read, click event, সবাই ওখানে অপেক্ষা করে। যে আগে আসে, সে আগে যায়।

Task মানে ওই queue থেকে নেওয়া একটা callback, যেটা শুরু থেকে শেষ পর্যন্ত একটানা চলে। কোনো task-কে মাঝপথে কেউ থামায় না। একে বলে "run to completion" নিয়ম, আর এজন্যই Rafi-র loop-এর মাঝে কেউ ঢুকতে পারেনি।

Microtask queue

আরেকটা ছোট লাইন আছে, যার অগ্রাধিকার বেশি: microtask queue। queueMicrotask(cb) দিয়ে সেখানে একটা callback রাখা যায়। Promise-এর .then(cb)-ও তার callback সেখানেই রাখে। Promise ঠিকঠাক শিখবে Module 13-এ; এখানে একটা then মানে শুধু "একটা microtask"।

অগ্রাধিকারের নিয়মটা এরকম: প্রতিটা task-এর পরে, আর main script-এর পরে, loop পরের task নেওয়ার আগে সব microtask চালিয়ে ফেলে। কাজ চলার সময় অন্য microtask নতুন যেগুলো রাখে, সেগুলোও এর মধ্যে পড়ে।

Loop: এক নিয়ম, তিন ধাপ

এবার পুরো ছবিটা তিনটা ধাপে এঁটে যায়, যেগুলো বারবার ঘোরে:

  1. চলতি task চালাও, যতক্ষণ না stack খালি হয়। প্রথম task হলো main script।
  2. সব microtask চালাও, পুরনোটা আগে, যতক্ষণ না microtask queue খালি হয়।
  3. Task queue-তে কিছু থাকলে সবচেয়ে পুরনো task-টা নাও, তারপর আবার ধাপ 1।

এটাই event loop। দুটো queue খালি, আর host-এর কাছে কোনো timer বা read বাকি নেই, এমন হলে Node-এর আর অপেক্ষার কিছু থাকে না, program শেষ হয়।

বিখ্যাত puzzle-টার ওপর loop-টা চলতে দেখো। Widget-টা program-টা আসল timer আর আসল microtask দিয়ে চালায়, আর প্রতিটা মুহূর্ত লিখে রাখে। নিচে program-এর নিজের output আসে, লাইন ধরে, ঠিক যে ক্রমে Node বানিয়েছে।

একই ধাপগুলো table-এ, প্রতি ধাপে একটা row।

ধাপLineStackWebapisTasksMicrotasksOutputকী হলো
11main scriptstartconsole.log চলে stack-এ, তাই "start" সাথে সাথে print হয়।
22main scripttimeoutstartsetTimeout "timeout"-কে host-এর timer-এর হাতে দিয়ে সাথে সাথে ফিরে আসে।
33main scripttimeoutthenstartthen "then"-কে microtask queue-তে রাখে।
44main scripttimeoutthen microstartqueueMicrotask "micro"-কে microtask queue-তে রাখে।
55main scripttimeoutthen microstart endconsole.log চলে stack-এ, তাই "end" সাথে সাথে print হয়।
65timeoutthen microstart endScript শেষ, stack খালি। আগে যাবে 2টা microtask।
73thentimeoutmicrostart end thenconsole.log চলে stack-এ, তাই "then" সাথে সাথে print হয়।
84microtimeoutstart end then microconsole.log চলে stack-এ, তাই "micro" সাথে সাথে print হয়।
92timeoutstart end then microTimer-এর সময় শেষ, তাই "timeout" task queue-তে দাঁড়ায়।
102timeoutstart end then micro timeoutconsole.log চলে stack-এ, তাই "timeout" সাথে সাথে print হয়।

Script শেষ হওয়ার মুহূর্তটা দেখো। Timer তখনো host-এর কাছে, আর দুটো microtask অপেক্ষায়। Loop-এর ধাপ 2 দুটোকেই চালায়। তার পরেই timer-এর callback task queue-তে আসে, তারপর stack-এ। শেষ frame-এ Node-এর আসল ক্রম: start, end, then, micro, timeout।

এক নজরে event loop

setTimeout(callback, ms);        // the host waits ms, then callback joins the TASK queue
queueMicrotask(callback);        // callback joins the MICROTASK queue
promise.then(callback);          // a microtask too (Module 13)

// the loop, forever:
// 1. run the current task until the stack is empty (first task: the main script)
// 2. run every microtask, new ones included
// 3. take the oldest task, go to 1
Example 1: তিন রকম লাইন

Lima নিজের শব্দ দিয়ে ক্রমটা দেখতে চায়। Program-টা log করে, microtask রাখে আর timer বসায়, প্রতিটা দুবার করে, এলোমেলো ক্রমে।

setTimeout(() => console.log("timer A"), 0);
console.log("sync 1");
queueMicrotask(() => console.log("micro A"));
setTimeout(() => console.log("timer B"), 0);
queueMicrotask(() => console.log("micro B"));
console.log("sync 2");
sync 1
sync 2
micro A
micro B
timer A
timer B

Script চলার সময়েই দুটো সাধারণ log print হয়। তারপর ধাপ 2 দুটো microtask চালায়, যে ক্রমে রাখা হয়েছিল। তারপর loop timer-গুলো তোলে, আগে A তারপর B, প্রতিটা একটা আলাদা task। প্রতিটা queue-এর ভেতরের ক্রম হলো call-গুলোর ক্রম।

Run in Compiler
Example 2: microtask-এর ভেতরে microtask

Script শেষ হওয়ার সময় যেগুলো অপেক্ষায় ছিল, loop শুধু সেগুলো চালিয়েই থামে না। নতুনগুলোও একই দফায় চলে।

setTimeout(() => console.log("timer"), 0);
queueMicrotask(() => {
  console.log("micro 1");
  queueMicrotask(() => console.log("micro 2, queued by micro 1"));
});
console.log("script done");
script done
micro 1
micro 2, queued by micro 1
timer

"micro 2" রাখা হয়েছে যখন microtask চলা শুরু হয়ে গেছে। তবু সে timer-এর আগে চলে, কারণ microtask queue সত্যি খালি না হওয়া পর্যন্ত ধাপ 2 চলতেই থাকে।

Run in Compiler

এটা কোথায় কাজে লাগে

  • Google Chrome: প্রতিটা tab তার page-এর JavaScript চালায় একটা main thread-এ, আর সেখানে লম্বা কোনো script শেষ না হওয়া পর্যন্ত scroll আর click আটকে থাকে।
  • Node.js: Node-এ বানানো একটা web server একটাই thread-এ অনেক request-এর উত্তর দেয়, কারণ database বা file-এর প্রতিটা অপেক্ষা host-এর হাতে চলে যায়।
  • Visual Studio Code: editor-টা Electron-এ বানানো, আর এর extension-গুলো চলে আলাদা একটা Node process-এ, তাই একটা ধীর extension টাইপ করা আটকায় না।

যে ভুলগুলো সবাই করে

1. শূন্যকে এখনই ভাবা

let ready = false;
setTimeout(() => {
  ready = true;
}, 0);
console.log("ready:", ready);
ready: false

Program print করে ready: false। Script শেষ না হওয়া পর্যন্ত timer-এর callback চলতে পারে না, তাই log আগে চলে। শূন্য delay মানে "stack খালি হলেই", কখনোই "ঠিক এখানেই" না।

2. ব্যস্ত loop দিয়ে timer-এর অপেক্ষা

let done = false;
setTimeout(() => {
  done = true;
}, 10);
const start = Date.now();
while (!done && Date.now() - start < 300) {}
console.log("done after the loop:", done);
done after the loop: false

Loop-টা অপেক্ষা করছে কখন done true হবে, কিন্তু stack আটকে রেখেছে সে নিজেই। Loop হাল না ছাড়া পর্যন্ত callback চলতে পারে না, তাই done false-ই থাকে। 300 ms-এর ছাড়টা না থাকলে এই program কখনো শেষ হতো না।

মাথা খাটাও

দুটো timer আর একটা microtask

Zara যত ছোট পারে একটা puzzle লেখে, আর বন্ধুদের কাছে output জানতে চায়।

setTimeout(() => console.log("1"), 0);
queueMicrotask(() => console.log("2"));
setTimeout(() => console.log("3"), 0);
console.log("4");

উত্তর দেখার আগে চারটা digit-এর ক্রমটা নিজে লিখে ফেলো।

Exercise 1সহজ

Lima একটা main script লেখে, তাতে তিন রকম লাইন। log X মানে console.log("X")। timeout X মানে setTimeout(() => console.log("X"), 0)। micro X মানে queueMicrotask(() => console.log("X"))। শব্দগুলো কোন ক্রমে screen-এ আসবে, Lima সেটা জানতে চায়।

Input. প্রথম লাইনে n। পরের n টা লাইনের প্রতিটায় একটা command, log, timeout বা micro, আর একটা শব্দ।

Output. n টা শব্দ, প্রতি লাইনে একটা, Node যে ক্রমে print করে সেই ক্রমে।

Constraints. 1 <= n <= 1000; Every word is 1 to 10 lowercase letters or digits..

Sample.

6
timeout tea
log one
micro milk
log two
timeout cake
micro sugar
one
two
milk
sugar
tea
cake
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));
Run in Compiler

queue-order নামে grade হয়।

Exercise 2মাঝারি

Bob সময় 0-তে একটা timer বসায়, তারপর তার script stack ব্যস্ত রাখে। Timer-এর সময় হলে আর stack খালি হলে তবেই callback চলে। Node 1-এর কম delay-কে 1 ধরে।

Input. q, তারপর q টা প্রশ্ন, প্রতিটায় একটা delay d আর script যে সময়ে শেষ হয় সেই s, ms-এ।

Output. প্রতিটা প্রশ্নের জন্য এক লাইন: callback যে সময়ে শুরু হয়।

Constraints. 1 <= q <= 1000। 0 <= d, s <= 1000000।

Sample. নিচের input।

4
100 30
0 0
20 500
5 5
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample-এর output.

100
1
500
5
Run in Compiler
Exercise 3কঠিন

Zara-র বন্ধুরা ওর puzzle-এর উত্তর আন্দাজ করে পাঠায়। একটা puzzle হলো log, timeout আর micro লাইনের একটা main script, প্রতিটার সাথে একটা শব্দ, Exercise 1-এর মতো। একটা আন্দাজ মানে শব্দগুলো কোনো একটা ক্রমে সাজানো।

Input. n, তারপর script-এর n টা লাইন। তারপর এক লাইনে আন্দাজের n টা শব্দ।

Output. আন্দাজটা Node-এর ক্রমের সাথে মিললে right, নইলে wrong at আর প্রথম যে জায়গায় মেলেনি তার নম্বর, 1 থেকে গুনে।

Constraints. 1 <= n <= 1000। শব্দগুলো 1 থেকে 10টা ছোট হাতের অক্ষর, সবগুলো আলাদা।

Sample. নিচের input।

4
timeout cat
log dog
micro owl
log ant
dog ant cat owl
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample-এর output.

wrong at 3
Run in Compiler

সচরাচর যে প্রশ্নগুলো আসে

  • Thread যদি একটাই হয়, আমার code চলার সময়েই file পড়া হয় কীভাবে?

    পড়ার কাজটা করে host, তোমার JavaScript-এর বাইরে। Node এজন্য operating system আর নিজের কয়েকটা সাহায্যকারী thread ব্যবহার করে। তুমি দেখো শুধু callback-টা, আর সেই callback চলে তোমার ওই একটা thread-এই। Lesson 05-এ অংশগুলোর নাম পাবে।

  • Task queue কি সত্যিই একটাই queue?

    Browser-এ task-এর কয়েকটা উৎস থাকতে পারে, যেমন timer আর click, আর browser এদের মধ্যে বেছে নেয়। Node তার loop-টাকে কয়েকটা phase-এ ভাগ করে, প্রতিটার নিজের queue। রোজকার code-এ "task-এর একটাই লাইন, আগে এলে আগে যাবে" ভাবলেই ঠিক ক্রম পাওয়া যায়। কোথায় ব্যাপারটা আরো খুঁটিনাটি, Lesson 05 দেখাবে।

  • Node timer-এর id-কে object হিসেবে print করে কেন?

    Node-এ setTimeout একটা Timeout object ফেরত দেয়, যার ref আর unref-এর মতো method আছে। Browser দেয় একটা সাধারণ সংখ্যা। দুটোরই আসল কাজ একটাই: clearTimeout-কে দেওয়া।

  • একটা Node program শেষ হয় কখন?

    যখন stack আর দুটো queue খালি, আর host-এর কাছে কিছু বাকি নেই: কোনো timer নেই, খোলা কোনো file বা socket নেই। এজন্যই চালু setInterval থাকা program নিজে থেকে কখনো শেষ হয় না।

মূল কথা

  • JavaScript চলে একটা thread আর একটা call stack-এ, আর চলতে থাকা task-কে কখনো মাঝপথে থামানো হয় না।
  • setTimeout callback-টাকে host-এর হাতে দিয়ে সাথে সাথে ফিরে আসে; অপেক্ষাটা করে host।
  • শেষ হওয়া timer-এর callback যায় task queue-তে; queueMicrotask আর then ব্যবহার করে microtask queue।
  • Loop: task শেষ করো, সব microtask চালাও, তারপর পরের task নাও।
  • শূন্য delay মানে "stack খালি হলেই", কখনোই "এখনই" না।
  • আরও গভীরে যেতে চাইলে: ভেতরের কলকবজা, libuv, Node-এর loop-এর phase, nextTick, setImmediate আর উপোসী loop (Pro)।

এরপর: হাতিয়ারগুলোর প্রতিটা, একে একে: setTimeout-এর আসল কথা, setInterval, বাতিল করা, আর callback-এর আকার।

lesson ১ শেষ

শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।

পরেরটা: Async-এর প্রতিটা হাতিয়ার: timer, বাতিল করা, microtask আর callback-এর আকার