Blocking vs Non-blocking — nima uchun xavfli

~15 daq · Node.js · Event Loop

Muammodan boshlaylik

Siz Node.js da server yozdingiz. Bitta endpoint bor — foydalanuvchi ma'lumotlarini qaytaradi. 100 ta foydalanuvchi bir vaqtda so'rov yubordi. Birinchi so'rov 3 soniya ishlaydi — qolgan 99 ta kutib turadi. Server "muzlab" qoldi.

Bu — blocking ning oqibati. Node.js da bu xato juda keng tarqalgan va topish qiyin, chunki kichik loyihalarda sezilmaydi. Faqat yuklanish oshganda ko'rinadi.


Blocking nima?

Blocking — Call Stack band bo'lib, Event Loop keyingi callbacklarni bajara olmaydigan holat.

Node.js bitta thread da ishlaydi. Agar shu thread band bo'lsa — boshqa hech narsa ishlamaydi. HTTP so'rovlar, taymerlar, I/O callbacklar — hammasi kutadi.

const http = require('http');

http.createServer((req, res) => {

  if (req.url === '/tez') {
    res.end('Tez javob');
    return;
  }

  if (req.url === '/sekin') {
    // ❌ Blocking — 5 soniya davomida hech narsa ishlamaydi
    const boshlanish = Date.now();
    while (Date.now() - boshlanish < 5000) {}
    res.end('Sekin javob');
  }

}).listen(3000);

/sekin ga so'rov yuborilganda — 5 soniya davomida /tez ga yuborilgan so'rovlar ham javob olmaydi. Bitta foydalanuvchi butun serverni "ushlab" turadi.


Blocking turlari

1. CPU-intensive hisoblash

// ❌ Blocking — katta hisoblash
function fibonacciSekin(n) {
  if (n <= 1) return n;
  return fibonacciSekin(n - 1) + fibonacciSekin(n - 2);
}

app.get('/fib', (req, res) => {
  const natija = fibonacciSekin(45); // bir necha soniya!
  res.json({ natija });
});

Bu endpoint ishlayotganda — boshqa barcha so'rovlar kutadi.

2. Sinxron fayl o'qish

const fs = require('fs');

// ❌ Blocking
app.get('/fayl', (req, res) => {
  const data = fs.readFileSync('katta-fayl.txt'); // kutadi
  res.send(data);
});

// ✅ Non-blocking
app.get('/fayl', (req, res) => {
  fs.readFile('katta-fayl.txt', (err, data) => {
    res.send(data);
  });
});

readFileSync — fayl o'qilguncha thread to'xtaydi. readFile — fayl o'qilishini OS ga topshiradi, thread bo'shaydi.

3. Sinxron JSON parse — katta ma'lumot

// ❌ Blocking — 10MB JSON
app.post('/import', (req, res) => {
  const data = JSON.parse(req.body); // katta bo'lsa sekin
  // ...
});

Kichik JSON uchun muammo yo'q. Lekin megabaytlik JSON parse qilish — bir necha millisekund bloklanish.

4. Sinxron kriptografiya

const crypto = require('crypto');

// ❌ Blocking
app.post('/register', (req, res) => {
  const hash = crypto.pbkdf2Sync(
    req.body.parol, 'salt', 100000, 64, 'sha512'
  ); // ~100ms bloklanish
  // ...
});

// ✅ Non-blocking
app.post('/register', (req, res) => {
  crypto.pbkdf2(
    req.body.parol, 'salt', 100000, 64, 'sha512',
    (err, hash) => {
      // ...
    }
  );
});

Non-blocking qanday ishlaydi?

Non-blocking kod — ishni OS yoki libuv ga topshiradi va thread ni bo'shatadi. Ish tugaganda callback chaqiriladi.

Blocking:
  Thread → fayl o'qi → [kutadi...] → natija → keyingi ish

Non-blocking:
  Thread → "fayl o'qi, tayyor bo'lganda xabar ber" → keyingi ish
                                    ↓
                              [OS fayl o'qiydi]
                                    ↓
                              callback chaqiriladi

Vizual farq:

Blocking (1 ta so'rov):
  ──────[3s hisoblash]──────► javob
  ──────────────────────────► (2-so'rov kutdi)
  ──────────────────────────► (3-so'rov kutdi)

Non-blocking (3 ta so'rov):
  ──[DB so'rov]──────────────► javob 1
  ──[DB so'rov]────────────► javob 2
  ──[DB so'rov]──────────────────► javob 3
  (hammasi parallel, thread bo'sh)

Bloklashni aniqlash

Oddiy test

const http = require('http');

http.createServer((req, res) => {
  if (req.url === '/blocking') {
    // Sintetik blok
    const end = Date.now() + 2000;
    while (Date.now() < end) {}
    res.end('OK');
    return;
  }
  res.end('Tez javob');
}).listen(3000);

Terminalda ikki oynada:

# 1-oyna
curl http://localhost:3000/blocking

# 2-oyna (darhol keyin)
curl http://localhost:3000/
# /blocking tugamaguncha bu ham kutadi!

Event Loop kechikishini o'lchash

let oxirgi = Date.now();

setInterval(() => {
  const hozir = Date.now();
  const kechikish = hozir - oxirgi - 100; // 100ms bo'lishi kerak edi
  if (kechikish > 10) {
    console.warn(`Event Loop kechikdi: ${kechikish}ms`);
  }
  oxirgi = hozir;
}, 100);

Agar bu 10ms dan ko'p kechikish ko'rsatsa — biror joyda blok bor.


Yechimlar

1. Asinxron versiyalarni ishlating

Node.js ning barcha sinxron funksiyalarining asinxron versiyasi bor:

// ❌ → ✅
fs.readFileSync   →  fs.readFile / fs.promises.readFile
fs.writeFileSync  →  fs.writeFile / fs.promises.writeFile
crypto.pbkdf2Sync →  crypto.pbkdf2
dns.lookupSync    →  dns.lookup

2. CPU-intensive ishni bo'laklarga bo'lish

// ❌ Blocking — katta massivni bir yo'la qayta ishlash
function hammaniQaytaIshla(massiv) {
  return massiv.map(element => ogirHisoblash(element));
}

// ✅ Non-blocking — setImmediate bilan bo'laklash
function bolaklabQaytaIshla(massiv, callback, index = 0, natija = []) {
  if (index >= massiv.length) {
    callback(natija);
    return;
  }
  natija.push(ogirHisoblash(massiv[index]));
  setImmediate(() => bolaklabQaytaIshla(massiv, callback, index + 1, natija));
}

3. Worker Threads — og'ir hisoblash uchun

CPU-intensive ish uchun eng to'g'ri yechim — alohida thread:

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  // Asosiy thread — server ishlaydi
  app.get('/fib', (req, res) => {
    const worker = new Worker(__filename, {
      workerData: { n: 45 }
    });
    worker.on('message', natija => res.json({ natija }));
  });

} else {
  // Worker thread — og'ir hisoblash
  function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
  parentPort.postMessage(fibonacci(workerData.n));
}

Worker thread alohida thread da ishlaydi — asosiy thread bloklanmaydi.


Qoidalar

✓ fs.readFile, fs.promises — readFileSync emas
✓ crypto.pbkdf2 — pbkdf2Sync emas
✓ DNS, tarmoq — har doim asinxron
✓ Katta JSON — stream bilan parse qiling
✓ CPU-intensive — Worker Threads ga topshiring
✗ while(true) — hech qachon asosiy threadda
✗ for (10M iteratsiya) — setImmediate bilan bo'lang
✗ Sync funksiyalar — faqat dastur boshlanishida (config o'qish)

💡 Istisno: readFileSync ni server ishga tushayotganda config fayl o'qish uchun ishlatish mumkin — chunki bu bir marta, so'rovlar kelmasdan oldin bajariladi.


Xulosa

Blocking — Node.js ning eng xavfli muammosi. Bitta bloklovchi kod butun serverni to'xtatadi.

Node.js bitta thread da ishlaydi — shu thread band bo'lsa, hech narsa ishlamaydi. Shuning uchun:

  • I/O uchun har doim asinxron versiyalar
  • CPU-intensive ish uchun Worker Threads
  • Katta hisoblashni setImmediate bilan bo'laklash

Blocking kichik loyihalarda sezilmaydi — faqat yuklanish oshganda ko'rinadi. Shuning uchun boshidanoq to'g'ri yozish muhim.


Keyingi dars

10-dars: Microtask va Macrotask — qaysi birinchi ishlaydi