Skip to content
DevRecall
← Roadmap
01

OS & Concurrency Basics

drafted

Process/thread, concurrency vs parallelism, blocking vs non-blocking I/O, race condition, deadlock, signals, container limits — góc nhìn của một backend Node.js.

0/43 rated · mastery 0%

Bài học

9 bài · ~136 phút đọc. Học theo thứ tự, rồi làm câu hỏi.
  1. 01~17 phút· 5 câuverify

    Process, thread & context switch

    Process sở hữu gì, thread chia sẻ gì, context switch tốn ở đâu, fork/exec và copy-on-write, zombie/orphan, và cách Node.js tạo process con an toàn.

  2. 02~14 phút· 5 câuverify

    Concurrency, scheduling & event loop

    Concurrency khác parallelism thế nào, preemptive vs cooperative scheduling, event loop của Node chạy ra sao, vì sao một đoạn code CPU-bound làm p99 của cả API tăng vọt, và Go/Java giải cùng bài toán khác Node ở đâu.

  3. 03~14 phút· 5 câuverify

    I/O models, epoll & libuv threadpool

    Blocking, non-blocking, multiplexing và async I/O; epoll/kqueue cho một thread phục vụ hàng nghìn socket ra sao; vì sao file I/O của Node chạy trên threadpool 4 thread; file descriptor và cách điều tra EMFILE.

  4. 04~14 phút· 5 câuverify

    Tận dụng nhiều core: cluster, worker_threads & shared memory

    cluster vs worker_threads vs nhiều container; chi phí thật của worker (khởi tạo, structured clone, transfer); SharedArrayBuffer, Atomics và memory model; khi nào tách việc CPU nặng thành service riêng.

  5. 05~15 phút· 6 câuverify

    Race condition trong code async: mutex, semaphore & backpressure

    Vì sao Node single-thread vẫn có race condition, check-then-act qua await, async mutex, semaphore và single-flight, giới hạn concurrency đúng cách, và backpressure trong stream.

  6. 06~17 phút· 6 câuverify

    Deadlock, livelock & locking ở database

    Bốn điều kiện Coffman và cách phá từng cái, deadlock/livelock/starvation trong backend thật, pessimistic vs optimistic locking, sửa oversell bằng atomic update, và điều tra deadlock giữa hai job trên PostgreSQL.

  7. 07~16 phút· 5 câuverify

    Distributed lock, fencing token & idempotency

    Redis lock với SET NX PX và các failure mode của nó, vì sao lock dựa trên TTL không đảm bảo loại trừ lẫn nhau, fencing token, idempotency, cron chạy trùng khi scale nhiều pod, và limiter theo tenant chống starvation.

  8. 08~16 phút· 5 câuverify

    Signals, PID 1 & graceful shutdown

    SIGTERM/SIGINT/SIGKILL/SIGHUP và exit code 128+n, cách Node xử lý signal, vì sao app làm PID 1 trong container bỏ qua SIGTERM, trình tự graceful shutdown đúng, và vòng đời pod Kubernetes khi deploy.

  9. 09~14 phút· 5 câuverify

    Container resources: cgroup CPU, memory & OOM killer

    cgroup giới hạn CPU và memory ra sao, CFS throttling, vì sao os.cpus() nói dối trong container, OOM killer và exit 137, giải phẫu memory của một process Node (RSS, heap, external) và cách đặt --max-old-space-size.

Tóm tắt & cheat sheet của track

TL;DR

  • Process = không gian địa chỉ + tài nguyên riêng; thread = luồng thực thi chia sẻ memory trong process. Node.js chạy JS trên một thread nhưng process có thêm libuv threadpool và V8 helper threads.
  • Concurrency là cấu trúc (nhiều việc tiến triển xen kẽ); parallelism là thực thi đồng thời trên nhiều core. Event loop cho concurrency, không cho parallelism CPU.
  • Race condition vẫn xảy ra trong JS single-threaded — mọi await là một điểm xen kẽ; và luôn xảy ra giữa nhiều instance/pod dùng chung DB/Redis.
  • Deadlock cần đủ 4 điều kiện Coffman; phá một điều kiện (thường là thứ tự lock cố định hoặc timeout) là hết.
  • Production: hiểu signals (SIGTERM → graceful shutdown), PID 1 trong container, file descriptor limit, cgroup memory/CPU limit và OOM killer.

Khái niệm cốt lõi

Khái niệmMột câuVí dụ ngắn
ProcessChương trình đang chạy với virtual memory, FD table, PID riêngMỗi node server.js là một process
ThreadLuồng thực thi trong process, chia sẻ heap, có stack riêngworker_threads tạo thread với V8 isolate riêng
Context switchOS lưu/khôi phục register để chuyển CPU giữa thread/process1000 thread blocking → CPU tốn cho switching
Blocking I/OThread đứng chờ tới khi I/O xongfs.readFileSync chặn event loop
Non-blocking I/O + multiplexingĐăng ký nhiều FD, kernel báo khi sẵn sàng (epoll/kqueue/IOCP)libuv poll phase
Race conditionKết quả phụ thuộc thứ tự xen kẽ không kiểm soátHai request cùng trừ tồn kho
Critical sectionĐoạn code truy cập shared state cần loại trừ lẫn nhauSELECT ... FOR UPDATE bao đoạn trừ stock
Mutex / SemaphoreMutex: 1 holder; semaphore: tối đa N holderSemaphore 10 = giới hạn 10 call đồng thời
DeadlockCác bên chờ nhau mãi mãiTx A lock row 1 chờ row 2, Tx B ngược lại
Atomic / CASThao tác không thể bị chen ngang; compare-and-swapUPDATE ... SET v = v + 1 WHERE version = 3
SignalThông báo bất đồng bộ từ OS tới processSIGTERM khi Kubernetes stop pod

Cơ chế hoạt động

Blocking vs non-blocking I/O trong một server

Check-then-act race qua await

Fix: một câu lệnh atomic UPDATE products SET stock = stock - 1 WHERE id = $1 AND stock > 0 rồi kiểm tra rowCount, hoặc SELECT ... FOR UPDATE trong transaction, hoặc optimistic locking theo version.

Graceful shutdown khi nhận SIGTERM

Trade-offs

Mô hìnhƯuNhượcHợp với
Thread-per-requestCode tuần tự dễ đọc, tận dụng nhiều coreMemory/thread, context switch, cần lock shared stateCPU-bound vừa, stack Java/.NET truyền thống
Event loop (Node)Hàng chục nghìn connection với ít memory, không lock cho JS stateMột tác vụ CPU chặn tất cả; chỉ 1 core cho JSI/O-bound API, gateway, realtime
Multi-process (cluster, nhiều pod)Tận dụng core, cô lập crashKhông share memory → state phải ra Redis/DBScale Node trên máy nhiều core
worker_threadsParallel CPU trong cùng process, share SharedArrayBufferChi phí khởi tạo isolate, message copyParse, nén, hash, image
Pessimistic lockĐơn giản, đúng ngayGiảm throughput, deadlockContention cao, xung đột đắt
Optimistic (version/CAS)Không giữ lock, throughput caoRetry khi conflictContention thấp

Pitfalls

  • Tin rằng "Node single-thread nên không có race condition".
  • Dùng API *Sync (readFileSync, bcrypt.hashSync, JSON.parse 50 MB) trong request path.
  • CMD npm start trong Dockerfile: npm là PID 1, có thể không forward SIGTERM → app bị SIGKILL sau grace period, mất request.
  • os.cpus().length trong container trả số core của host, không phải CPU quota → spawn quá nhiều worker; dùng os.availableParallelism() và vẫn kiểm tra cgroup quota.
  • Không đặt --max-old-space-size phù hợp memory limit của container → OOMKilled thay vì GC sớm.
  • Lock phân tán (Redis) không có TTL/fencing token → lock "vĩnh viễn" hoặc hai holder cùng lúc.
  • Unbounded Promise.all trên 10k item → cạn connection pool / FD.

Cheat sheet

  • Coffman: mutual exclusion, hold-and-wait, no preemption, circular wait → phá circular wait bằng lock ordering.
  • Deadlock (không ai tiến) · Livelock (bận rộn nhưng không tiến) · Starvation (một bên không bao giờ được chạy).
  • SIGTERM bắt được (graceful) · SIGKILL/SIGSTOP không bắt được · SIGINT = Ctrl+C · exit code 137 = 128 + 9 (SIGKILL, thường OOM) · 143 = 128 + 15 (SIGTERM).
  • libuv threadpool: fs, dns.lookup, crypto (pbkdf2, scrypt, randomBytes async), zlib; network socket không dùng threadpool. UV_THREADPOOL_SIZE mặc định 4.
  • Zombie = process con đã exit nhưng cha chưa wait(); orphan = cha chết trước, được init nhận nuôi.
  • "EMFILE: too many open files" → FD limit (ulimit -n) hoặc leak socket/file.

Interview Questions

Showing 43 / 43

async function redeem(code: string, userId: string) {
  const c = await db.coupon.find(code);
  if (c.usedBy) throw new Error('used');
  await db.coupon.update(code, { usedBy: userId }); // two requests can both pass the check
}
FROM node:24-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
CMD ["npm", "start"]
process.on('SIGTERM', () => {
  console.log('bye');
  process.exit(0);
});
UPDATE orders
   SET status = 'paid', version = version + 1
 WHERE id = $1 AND version = $2;
-- rowCount = 0 → someone else changed it: reload and decide
let token: string | null = null;
let expiresAt = 0;

export async function getToken() {
  if (!token || Date.now() > expiresAt) {
    const r = await fetch('https://auth.internal/token', { method: 'POST' });
    const body = await r.json();
    token = body.access_token;
    expiresAt = Date.now() + body.expires_in * 1000;
  }
  return token;
}
import { spawn } from 'node:child_process';
const p = spawn('ffmpeg', ['-i', inputPath, '-vf', 'scale=640:-1', outPath]);
p.on('exit', (code, signal) => log.info({ code, signal }, 'ffmpeg done'));
import { pipeline } from 'node:stream/promises';
await pipeline(fs.createReadStream('big.csv'), parseCsv(), transformRow(), uploadStream);
-- release.lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1])
end
return 0
async function reserve(productId: string, qty: number) {
  const { stock } = await db.one('SELECT stock FROM products WHERE id = $1', [productId]);
  if (stock < qty) throw new OutOfStock();
  await db.none('UPDATE products SET stock = $1 WHERE id = $2', [stock - qty, productId]);
}
BEGIN;
SELECT id FROM orders WHERE id = ANY($1) ORDER BY id FOR UPDATE;
UPDATE orders SET status = 'shipped' WHERE id = ANY($1);
COMMIT;
const server = app.listen(3000);

process.on('SIGTERM', async () => {
  server.close();
  await db.end();
  await redis.quit();
  process.exit(0);
});
async function runLimited<T>(tasks: (() => Promise<T>)[], limit: number) {
  const results: T[] = [];
  let i = 0;
  const worker = async () => {
    while (i < tasks.length) {
      results.push(await tasks[i++]());
    }
  };
  Array.from({ length: limit }, worker);
  return results;
}