IlmHamroh
JavaScript Full-stack/24-qism. Node.js36/55-dars17 daqiqa
Mundarija (31)

Node.js worker_threads: og'ir hisobni alohida thread'ga chiqarish

Qisqacha: worker_threads — Node'da JavaScript'ni alohida thread'da ishga tushirish. Og'ir hisob (rasm, shifr, katta tahlil) asosiy thread'da bo'lsa, event loop qotadi: kanon o'lchovida taymerlar 411 ms kechikdi. Xuddi shu hisob 4 ta worker'da — kechikish 16 ms gacha tushdi. Worker bepul emas: har biri ~20 ms da ishga tushadi va o'z xotirasini oladi. Ma'lumot nusxalanadi, transferList bilan esa ko'chiriladi.

Bu darsda

  • Event loop bloklanishini taymer kechikishi bilan o'lchab ko'rsata olasiz.
  • Worker'ni Promise'ga o'rab, natija va xatoni to'g'ri ushlaysiz.
  • transferList, MessageChannel va SharedArrayBuffer farqini bilasiz.
  • resourceLimits va terminate() bilan worker'ni cheklaysiz.
  • Worker qachon kerak, qachon ortiqcha ekanini o'lchov bilan aytasiz.

Oldin bilishingiz kerak: Web Workers (Node'dagi birinchi worker), SharedArrayBuffer va Atomics, Event loop'ni bloklash, libuv thread pool.

1. Nega bu kerak?

Web Workers darsining oxirida Node'dagi egizakni ko'rgan edingiz: node:worker_threads, isMainThread, workerData, parentPort. O'sha yerda "Node'da worker'lar server qismida og'ir ishlar uchun ishlatiladi" degan edik. Mana o'sha qism.

Event loop'ni bloklash darsidan esda: Node'da JavaScript bitta thread'da ishlaydi. Bir funksiya 400 ms hisoblasa, shu 400 ms davomida hech narsa bo'lmaydi: taymerlar kutadi, kelgan so'rovlar navbatda turadi. Serverda bu "sayt qotib qoldi" degani. libuv thread pool faylni o'qish, siqish kabi ishlarni allaqachon boshqa thread'larda bajaradi. Lekin sizning JavaScript kodingiz — doim asosiy thread'da. Uni boshqa thread'ga faqat worker_threads chiqaradi.

Hayotiy o'xshatish: choyxonada bitta choyxonachi. U buyurtma oladi, choy damlaydi, pul sanaydi. Bir mehmon 200 ta somsa buyurtma qildi va choyxonachi somsa yopishga tushib ketdi — boshqa mehmonlar bir soat choy kutadi. Aqlli egasi somsa uchun alohida novvoy yollaydi. Choyxonachi esa buyurtmani novvoyga uzatadi va mehmonlarga qaytadi. Worker — o'sha novvoy.

2. Bloklanishni o'lchaymiz

2.1 Taymer kechikishi

Event loop qotganini qanday ko'rish mumkin? Eng oddiy usul — 10 ms li taymer qo'yib, u qachon ishlaganini o'lchash. Loop bo'sh bo'lsa, taymer ~10 ms da ishlaydi. Loop band bo'lsa — kechikadi. Bir xil hisobni (fib(36) — bizning kompyuterda ~150 ms) avval asosiy thread'da, keyin worker'da bajaramiz:

js
import {
  Worker, isMainThread, parentPort, workerData,
} from "node:worker_threads";

const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));

function workerda(n) {
  return new Promise((ok) => {
    const fayl = new URL(import.meta.url);
    const w = new Worker(fayl, { workerData: n });
    w.once("message", ok);
  });
}

async function kechikishniOlcha(ish) {
  const boshi = performance.now();
  let kechikish = 0;
  setTimeout(() => {
    kechikish = performance.now() - boshi - 10;
  }, 10);
  await ish();
  await new Promise((ok) => setTimeout(ok, 30));
  return kechikish;
}

if (isMainThread) {
  const a = await kechikishniOlcha(async () => fib(36));
  const b = await kechikishniOlcha(() => workerda(36));
  console.log("asosiy thread'da — taymer kechikdimi:", a > 50);
  console.log("worker'da — taymer kechikdimi:", b > 50);
} else {
  parentPort.postMessage(fib(workerData));
}

Konsolda:

text
asosiy thread'da — taymer kechikdimi: true
worker'da — taymer kechikdimi: false

Aniq millisekundlar kompyuterga bog'liq, shuning uchun "50 ms dan ko'pmi?" deb so'radik. Asosiy thread'da fib(36) tugamaguncha taymer navbatda turdi. Worker bilan esa asosiy thread bo'sh qoldi: taymer o'z vaqtida ishladi, fib esa boshqa thread'da hisoblandi.

Kodda bitta fayl ikki rolni o'ynaydi: isMainThread true bo'lsa — boshqaruvchi, false bo'lsa — worker. new URL(import.meta.url) — "shu faylning o'zi". Bu naqsh Web Workers darsidan tanish.

2.2 Kanon o'lchovi

Kanon aniqroq vosita ishlatdi — perf_hooks dagi monitorEventLoopDelay. U event loop kechikishini histogramma sifatida yig'adi va eng katta qiymatni beradi. Vazifa: 4 marta fib(36) (tajriba/t36-worker.mjs, xom t36-worker.txt, Node 24.21.0, Windows 11, i5-12500H — 16 mantiqiy yadro):

text
asosiy oqim: 4 × fib(36) 422 ms, loop kechikishi max 411 ms
4 worker: 4 × fib(36) 178 ms (worker yaratish ham ichida), loop kechikishi max 16 ms
bitta bo'sh worker ishga tushishi: 20.0 ms
SharedArrayBuffer + Atomics: worker qo'shdi → 5
4 × fib(36): event loop'ning eng katta kechikishi
  • Asosiy thread422 ms ish411 ms
  • 4 worker178 ms ish16 ms

Manba: IlmHamroh kanon o'lchovi, 2026-10-23: Node 24.21.0, Windows 11, i5-12500H (t36-worker.txt)

Ikki foyda ko'rinadi:

  1. Javob beruvchanlik: kechikish 411 ms dan 16 ms ga tushdi. Asosiy thread bu vaqtda boshqa so'rovlarga javob bera oladi.
  2. Tezlik: 422 ms dan 178 ms ga. To'rtta hisob to'rt yadroda parallel ketdi. Bu — 4 baravar emas, ~2.4 baravar: worker'larni yaratish, ishga tushirish va xabar almashish vaqti ham ichida.

16 ms kechikish nimadan? Asosiy thread worker'larni yaratayotgan va xabarlarni qabul qilayotgan paytda ham band bo'ladi — oz bo'lsa ham.

Tekshirib ko'ring: Kanondagi valyuta tarix 34 kunni 36 soniyada oladi. Uni worker'larga bo'lsak, tezlashadimi?

Javob

Yo'q. Vaqt CBU serverining javobini kutishga ketadi (har so'rov ~1 s), JavaScript hisobiga emas. Kutish paytida asosiy thread allaqachon bo'sh — fetch asinxron. Worker faqat JavaScript'ning o'zi uzoq hisoblaganda yordam beradi. Tarmoqni tezlashtirish uchun so'rovlarni parallel yuborish kerak bo'lardi — kanon buni CBU'ga yuk bermaslik uchun ataylab qilmadi.

3. Worker'ni Promise'ga o'rash va xatolar

3.1 Uch hodisa

Worker bilan ishlashda uchta hodisa muhim:

Hodisa Qachon Nima keladi
message worker parentPort.postMessage(x) qildi x (nusxasi)
error worker ichida ushlanmagan xato xato obyekti
exit worker tugadi (har qanday sabab bilan) chiqish kodi

Ko'p kodda faqat message tinglanadi — bu tuzoq. Worker xato bilan yiqilsa, message hech qachon kelmaydi va Promise abadiy kutadi. Xuddi pipeline darsidagi .pipe() holati. To'g'ri o'ram uchala hodisani tinglaydi:

js
import {
  Worker, isMainThread, parentPort, workerData,
} from "node:worker_threads";

if (isMainThread) {
  function hisobla(n) {
    return new Promise((resolve, reject) => {
      const fayl = new URL(import.meta.url);
      const w = new Worker(fayl, { workerData: n });
      w.once("message", resolve);
      w.once("error", reject);
      w.once("exit", (kod) => {
        if (kod !== 0) reject(new Error(`worker ${kod} kod bilan`));
      });
    });
  }
  console.log("fib(25):", await hisobla(25));
  try {
    await hisobla(-1);
  } catch (xato) {
    console.log("xato:", xato.message);
  }
} else {
  if (workerData < 0) throw new Error("n manfiy bo'lmasin");
  const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));
  parentPort.postMessage(fib(workerData));
}

Konsolda:

text
fib(25): 75025
xato: n manfiy bo'lmasin

Worker ichidagi throw asosiy thread'da error hodisasi bo'lib keldi. Xato obyekti structured clone (tuzilmali nusxa) bilan o'tadi — xabari va nomi saqlanadi. Keyin exit 1 kodi bilan keladi, lekin Promise allaqachon rad etilgan — ikkinchi reject jim e'tiborsiz qoladi.

exit tinglovchisi nima uchun kerak? Worker process.exit(2) qilsa yoki terminate() bilan to'xtatilsa, error bo'lmaydi — faqat exit. Bu tinglovchisiz Promise yana osilib qolardi.

3.2 Diagramma: kim kimga nima yuboradi

sequenceDiagram
  participant A as Asosiy thread
  participant W as Worker
  A->>W: new Worker(fayl, { workerData })
  Note over A: bo'sh — so'rovlarga javob beradi
  W->>W: fib(36) hisoblaydi
  W-->>A: message: natija
  W-->>A: exit: 0
  Note over W: xato bo'lsa: error, keyin exit: 1

3.3 terminate() va resourceLimits

Worker'ni tashqaridan to'xtatish — w.terminate(). U worker'ni darhol o'ldiradi (ish o'rtasida bo'lsa ham) va exit 1 kodi bilan keladi. Foydalanuvchi "bekor qilish" ni bosganda yoki vaqt chegarasi tugaganda ishlatiladi.

Worker xotirasini cheklash ham mumkin — resourceLimits. Quyidagi worker cheksiz massiv yig'adi, biz esa unga 16 MB heap beramiz:

js
import { Worker } from "node:worker_threads";

const kod = "const a = []; for (;;) a.push(new Array(1e5).fill(1));";
const w = new Worker(kod, {
  eval: true,
  resourceLimits: { maxOldGenerationSizeMb: 16 },
});
w.on("error", (x) => console.log(x.code));
w.on("exit", (k) => console.log("exit:", k));

Konsolda:

text
ERR_WORKER_OUT_OF_MEMORY
exit: 1

To'liq xabar: Worker terminated due to reaching memory limit: JS heap out of memory — "worker xotira chegarasiga yetgani uchun to'xtatildi". Muhimi: faqat worker o'ldi, asosiy jarayon tirik qoldi. Katta fayl darsida --max-old-space-size=16 butun jarayonni FATAL ERROR bilan yiqitgan edi. Ishonchsiz ma'lumot bilan og'ir ish (foydalanuvchi yuklagan faylni tahlil qilish) — worker + resourceLimits uchun klassik holat.

eval: true — birinchi argument fayl yo'li emas, kod matni. Kichik misollar uchun qulay. Worker ichida u CommonJS skript sifatida bajariladi (require). Real loyihada esa alohida fayl yozing — uni tahrirlash, lint va test qilish oson.

Tekshirib ko'ring: hisobla() o'ramidan exit tinglovchisini olib tashlasak va worker process.exit(0) bilan, postMessage qilmasdan chiqsa — nima bo'ladi?

Javob

Promise hech qachon bajarilmaydi: message kelmadi, error ham bo'lmadi (chiqish kodi 0 — xato emas). Shuning uchun to'liq o'ramda exit da kod !== 0 tekshiruvidan tashqari "natija kelmay chiqdi" holatini ham ushlash mumkin. Masalan, exit da reject(new Error("natijasiz chiqdi")) — agar Promise allaqachon bajarilgan bo'lsa, bu chaqiruv e'tiborsiz qoladi.

4. Ma'lumot uzatish: nusxa, ko'chirish, umumiy xotira

4.1 Nusxa va transferList

postMessage sukut bo'yicha nusxa yuboradi (structured clone). 8 MB massiv yuborsangiz, xotirada ikkita 8 MB bo'ladi va nusxalashga vaqt ketadi. ArrayBuffer ni esa ko'chirish mumkin: egalik worker'ga o'tadi, sizda esa bo'sh (byteLength: 0) bufer qoladi. Buning uchun ikkinchi argument — transferList:

js
import { Worker } from "node:worker_threads";

const kod = `const { parentPort } = require("node:worker_threads");
parentPort.on("message", (b) => {
  parentPort.postMessage(b.byteLength);
});`;
const w = new Worker(kod, { eval: true });
const nusxa = new ArrayBuffer(8_000_000);
const kochir = new ArrayBuffer(8_000_000);
w.postMessage(nusxa);
console.log("nusxadan keyin:", nusxa.byteLength);
w.postMessage(kochir, [kochir]);
console.log("ko'chirishdan keyin:", kochir.byteLength);
let n = 0;
w.on("message", (x) => {
  console.log("worker oldi:", x);
  if (++n === 2) w.terminate();
});
w.on("exit", (k) => console.log("exit kodi:", k));

Konsolda:

text
nusxadan keyin: 8000000
ko'chirishdan keyin: 0
worker oldi: 8000000
worker oldi: 8000000
exit kodi: 1

Worker ikkala holatda ham 8 MB oldi. Lekin ko'chirilgan bufer asosiy thread'da endi bo'sh — uni o'qisangiz, hech narsa topmaysiz. Bu Web Workers darsidagi transfer bilan bir xil. terminate() dan keyin exit kodi 1 bo'ldi — "tashqaridan to'xtatildi".

terminate() ni nega chaqirdik? Worker ichida parentPort.on("message") tinglovchisi bor — u worker'ni tirik ushlab turadi. Ish tugagach worker'ni o'zingiz yopmasangiz, dastur hech qachon tugamaydi.

4.2 MessageChannel: alohida aloqa kanali

parentPort — worker va uni yaratgan thread orasidagi yagona kanal. Ba'zan ko'proq kerak: ikki worker'ni to'g'ridan-to'g'ri bog'lash yoki har vazifaga alohida javob kanali. MessageChannel ikki uchli quvur yasaydi — port1 va port2. Bir uchini worker'ga beramiz (portni ham ko'chirish kerak — transferList):

js
import { Worker, MessageChannel } from "node:worker_threads";

const kod = `const { workerData } = require("node:worker_threads");
workerData.port.postMessage("salom, men " + workerData.nom);
workerData.port.close();`;
const { port1, port2 } = new MessageChannel();
port1.on("message", (x) => console.log("port1 oldi:", x));
new Worker(kod, {
  eval: true,
  workerData: { port: port2, nom: "hisobchi" },
  transferList: [port2],
});

Konsolda:

text
port1 oldi: salom, men hisobchi

Worker javobni parentPort orqali emas, unga berilgan port2 orqali yubordi. port.close() kanalni yopadi — aks holda ochiq port dasturni tirik ushlab turardi. Bu naqsh worker pool'larning ichida ishlatiladi: har vazifa o'z javob kanalini oladi. Pool haqida keyingi darsda.

4.3 SharedArrayBuffer: umumiy xotira

Uchinchi yo'l — xotirani umuman nusxalamaslik, balki bo'lishish. SharedArrayBuffer va Atomics darsida buni Node'da sinab ko'rgansiz, race condition va Atomics.add bilan. Kanon ham shuni tekshirdi: worker umumiy katakka Atomics.add(..., 5) qildi va asosiy thread 5 ni ko'rdi (yuqoridagi xom chiqishning oxirgi qatori). Brauzerdan farqli, Node'da SharedArrayBuffer hech qanday sarlavhasiz (COOP/COEP) ishlaydi.

Usul Narx Qachon
Nusxa (sukut) xotira × 2, nusxalash vaqti kichik ma'lumot, oddiy holat
transferList deyarli bepul, lekin sizda bo'sh qoladi katta bufer bir tomonga ketadi
SharedArrayBuffer bepul, lekin Atomics va ehtiyot kerak ko'p thread bitta ma'lumotda ishlaydi

5. Worker'ning narxi va qachon kerak

5.1 Narxi

Kanon o'lchovi: bitta bo'sh worker 20.0 ms da ishga tushadi. Har worker — alohida V8 nusxasi: o'z heap'i, o'z event loop'i, o'z modul keshi. Xotirada ham bir necha MB oladi.

Shuning uchun har kichik vazifaga yangi worker ochish — xato. 1 ms lik hisob uchun 20 ms lik worker — 20 baravar sekinroq. Ko'p kichik vazifa bo'lsa, worker'larni oldindan ochib, qayta ishlatish kerak — bu worker pool. Uni keyingi darsda Piscina bilan o'lchaymiz.

5.2 Qachon kerak

Ish Worker kerakmi? Sabab
Rasm o'lchamini o'zgartirish, PDF yasash ha JS/WASM hisobi, 100+ ms
Katta JSON'ni JSON.parse ba'zan 50+ MB bo'lsa asosiy thread qotadi
Parolni hash qilish (scrypt, argon2) yo'q Node uni o'zi thread pool'da bajaradi
Fayl o'qish, fetch, baza so'rovi yo'q allaqachon asinxron, kutish — hisob emas
Kichik hisob (< 10 ms) yo'q worker ochish undan qimmat

Parolni hash qilish haqida: crypto.scrypt ning asinxron varianti (crypto 2) ishni libuv thread pool'iga beradi. Uni worker'ga o'rash — ortiqcha qatlam.

5.3 Kanon qarori: valyuta ga worker kerak emas

Kanon valyuta ning eng og'ir ishlarini o'lchadi: kurslarni formatlash — mikrosekundlar, 1 million qatorli tahlil — 796 ms, lekin u fayl o'qish bilan aralash va xotirasi barqaror. CLI bir martalik buyruq, parallel foydalanuvchi yo'q — "event loop qotdi" muammosi bu yerda bo'lmaydi. Worker qo'shilsa, kod murakkablashadi va har ishga tushishga ~20 ms qo'shiladi. Shuning uchun worker_threads faqat tajribada qoldi. Bu dars uchun "CLI qadami" o'rnida — o'sha tajriba va 4-mashq.

6. Ko'p uchraydigan xatolar

6.1 Faqat message ni tinglash

Worker yiqilsa, Promise abadiy kutadi. message, error va exit — uchalasi («Uch hodisa» bo'limidagi o'ram).

6.2 Worker'da tinglovchini yopmaslik

parentPort.on("message") yoki ochiq MessagePort worker'ni (va butun dasturni) tirik ushlab turadi. Dastur "tugamaydi". Ish tugagach w.terminate(), port.close() yoki parentPort.close().

6.3 Funksiyani yuborish

js
// ❌ funksiya structured clone bilan o'tmaydi
new Worker(fayl, { workerData: { hisobla: (x) => x * 2 } });

Natija: DataCloneError: (x)=>x*2 could not be cloned. — "nusxalab bo'lmadi". Funksiya worker faylining o'zida yozilsin, unga faqat ma'lumot yuboring (Web Workers darsidagi qoida).

6.4 Ko'chirilgan buferni ishlatish

transferList dan keyin bufer bo'sh: byteLength — 0, Uint8Array orqali o'qish hech narsa bermaydi. Ko'chirgandan keyin o'sha o'zgaruvchiga tegmang yoki worker javobda qaytarib yuborsin.

6.5 Har kichik vazifaga yangi worker

20 ms ishga tushish × 1000 vazifa = 20 soniya behuda. Pool (keyingi dars).

7. Mashqlar

1-mashq (oson): Qaysi biri worker'ga?

Har vazifa uchun javob bering — worker kerakmi va nega:

  1. Telegram bot foydalanuvchi yuborgan rasmni 3 xil o'lchamga kichraytiradi (har biri ~150 ms JS hisob).
  2. Server har so'rovda bazadan foydalanuvchi ma'lumotini oladi.
  3. CLI 10 ta valyuta kursini jadval qilib chiqaradi.
Yechim
  1. Ha. 150 ms × 3 = 450 ms asosiy thread'da — bu vaqtda bot boshqa foydalanuvchilarga javob bermaydi. Worker (yoki pool) bilan asosiy thread bo'sh qoladi. Rasm kutubxonalarining ko'pi (masalan sharp) buni ichkarida o'zi thread pool'da qiladi — avval shuni tekshiring.
  2. Yo'q. Baza so'rovi — kutish, hisob emas; asosiy thread bu vaqtda bo'sh.
  3. Yo'q. Mikrosekundlik ish; worker ochish (~20 ms) undan minglab marta qimmat.

2-mashq (o'rta): Vaqt chegarali worker

vaqtBilan(n, ms) funksiyasini yozing: fib(n) ni worker'da hisoblasin, lekin ms millisekunddan oshsa worker'ni terminate() qilib, "vaqt tugadi" xabarli xato bilan rad etsin. Sinov: vaqtBilan(20, 2000) — natija; vaqtBilan(45, 100) — xato. Eslatma: setTimeout va clearTimeout (taymerlar).

Yechim
js
import {
  Worker, isMainThread, parentPort, workerData,
} from "node:worker_threads";

const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));

function vaqtBilan(n, ms) {
  return new Promise((resolve, reject) => {
    const fayl = new URL(import.meta.url);
    const w = new Worker(fayl, { workerData: n });
    const taymer = setTimeout(() => {
      w.terminate();
      reject(new Error("vaqt tugadi"));
    }, ms);
    w.once("message", (x) => {
      clearTimeout(taymer);
      resolve(x);
    });
    w.once("error", (x) => {
      clearTimeout(taymer);
      reject(x);
    });
  });
}

if (isMainThread) {
  console.log("fib(20):", await vaqtBilan(20, 2000));
  try {
    await vaqtBilan(45, 100);
  } catch (xato) {
    console.log("xato:", xato.message);
  }
} else {
  parentPort.postMessage(fib(workerData));
}

Konsolda:

text
fib(20): 6765
xato: vaqt tugadi

fib(45) bir necha soniya oladi — 100 ms da u to'xtatildi. terminate() siz worker orqa fonda hisoblashda davom etar va dastur tugashini kutib turardi. clearTimeout esa tez tugagan holatda ortiqcha terminate chaqirilmasligi uchun.

3-mashq (qiyin): Ishni yadrolarga bo'lish

1 dan 2 000 000 gacha tub sonlar sonini 4 ta worker'ga bo'lib hisoblang: har worker o'z oralig'ini oladi ([boshi, oxiri)), natijalar qo'shiladi. Natija bitta thread'dagi hisob bilan bir xil bo'lsin. Eslatma: Promise.all (Promise kombinatorlari); tub son tekshiruvi — Web Workers darsidagi countPrimes g'oyasi.

Yechim
js
import {
  Worker, isMainThread, parentPort, workerData,
} from "node:worker_threads";

function tublarSoni(boshi, oxiri) {
  let soni = 0;
  for (let n = Math.max(boshi, 2); n < oxiri; n++) {
    let tub = true;
    for (let d = 2; d * d <= n; d++) {
      if (n % d === 0) { tub = false; break; }
    }
    if (tub) soni++;
  }
  return soni;
}

if (isMainThread) {
  const CHEGARA = 2_000_000;
  const qadam = CHEGARA / 4;
  const ishlar = [0, 1, 2, 3].map((i) => new Promise((ok, yoq) => {
    const w = new Worker(new URL(import.meta.url), {
      workerData: [i * qadam, (i + 1) * qadam],
    });
    w.once("message", ok);
    w.once("error", yoq);
  }));
  const qismlar = await Promise.all(ishlar);
  const jami = qismlar.reduce((a, b) => a + b, 0);
  console.log("qismlar:", qismlar.join(" + "));
  const bittada = tublarSoni(0, CHEGARA);
  console.log("jami:", jami, "| bitta thread:", bittada);
} else {
  parentPort.postMessage(tublarSoni(...workerData));
}

Konsolda:

text
qismlar: 41538 + 36960 + 35657 + 34778
jami: 148933 | bitta thread: 148933

Qismlar teng emas: katta sonlarda tub sonlar siyrakroq, lekin har sonni tekshirish qimmatroq. Shuning uchun oxirgi worker eng uzoq ishlaydi — oddiy "teng bo'lish" ishni teng taqsimlamaydi. Pool'lar buni ishni kichik bo'laklarga bo'lib, bo'shagan worker'ga keyingisini berish bilan hal qiladi. Birinchi qism 41 538 — Web Workers darsidagi 500 000 gacha natija bilan bir xil.

4-mashq: CLI qadami — tajriba, loyihaga kirmaydi

Bu darsda valyuta ga kod qo'shilmaydi. O'rniga kanon tajribasini o'zingiz takrorlang: kichik skriptda monitorEventLoopDelay({ resolution: 1 }) ni yoqing, 4 × fib(36) ni avval asosiy thread'da, keyin 4 ta worker'da bajaring va h.max / 1e6 (ms) ni solishtiring. Natijangizni kanon bilan (411 va 16 ms) taqqoslang va valyuta ga worker nega kerak emasligini bir gapda yozing.

Yechim

Kanon skriptining asosiy qismi (tajriba/t36-worker.mjs, qisqartirilgan):

js
import { monitorEventLoopDelay } from "node:perf_hooks";

const h = monitorEventLoopDelay({ resolution: 1 });
h.enable();
await new Promise((r) => setTimeout(r, 10));
for (let i = 0; i < 4; i++) fib(36);
await new Promise((r) => setTimeout(r, 5)); // kechikish yozilsin
console.log(`max ${(h.max / 1e6).toFixed(0)} ms`);

Raqamlaringiz yadro soni va protsessorga bog'liq bo'ladi; kanon — i5-12500H, 16 mantiqiy yadro. Gap namunasi: "valyuta da og'ir JS hisobi yo'q (1 mln qator tahlili ham 0.8 s va xotirasi barqaror), CLI'da parallel so'rovlar yo'q — worker faqat murakkablik va +20 ms ishga tushish qo'shadi."

8. Real ishda

  • Rasm va video: sharp (rasm) va boshqa kutubxonalar og'ir ishni ichkarida thread'larga chiqaradi. O'zingiz yozgan og'ir JS/WASM hisobini esa worker'ga olasiz.
  • Build vositalari: Vite, esbuild, Jest/Vitest kabi vositalar fayllarni bir nechta worker'da parallel qayta ishlaydi — 22-qismdagi Vitest "pool" sozlamasi aynan shu.
  • Serverlar: HTTP serverda (25-qism) og'ir hisobni so'rov ichida bajarish — butun serverni qotiradi. Worker yoki pool bilan server boshqa foydalanuvchilarga javob beraveradi.
  • Intervyu savollari: "Node bir threadli bo'lsa, CPU-og'ir ishni qanday qilasiz?", "worker_threads va child_process farqi?", "Worker'ga ma'lumot qanday uzatiladi va narxi qanday?".

Xulosa

  • Og'ir JS hisobi asosiy thread'da event loop'ni qotiradi: kanonda 411 ms kechikish, 4 worker bilan — 16 ms.
  • Worker'ni Promise'ga o'rashda message, error, exit — uchalasini tinglang.
  • Ma'lumot nusxalanadi; ArrayBuffer ni transferList bilan ko'chirish, SharedArrayBuffer bilan bo'lishish mumkin.
  • resourceLimits worker'ni cheklaydi — xotira tugasa faqat worker o'ladi; terminate() — tashqaridan to'xtatish.
  • Worker ~20 ms da ishga tushadi: kichik vazifalar uchun ortiqcha, ko'p vazifaga — pool.

Keyingi dars: Worker pool: Piscina — worker'larni qayta ishlatib, 16 vazifani 102 ms o'rniga 39 ms da bajaramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Node.js workerthreads: og'ir hisobni alohida thread'ga chiqarish — IlmHamroh