CHƯƠNG 03 · PROCESS · ~110 phút

Thread &
Concurrency Models

Process "nặng", thread "nhẹ". Đây là chương cực hay hỏi trong phỏng vấn: process vs thread, threading models, JavaScript event loop, Worker Threads. Hiểu rõ chương này, bạn trả lời được câu kinh điển: "Node.js single-threaded, làm sao xử lý hàng nghìn request đồng thời?"

3.1 Thread là gì?

Thread là đơn vị thực thi nhỏ hơn process. Một process có thể chứa nhiều thread, các thread chia sẻ memory của process. Vì thế thread thường được gọi là "lightweight process".

┌──────────── PROCESS ────────────┐ │ │ │ Code (text) │ Data │ Heap │ ← chia sẻ giữa các thread │ │ ├─────────────────────────────────┤ │ Thread 1 │ Thread 2 │ Thread 3│ │ - stack 1 │ - stack 2│ -stack 3│ ← mỗi thread stack riêng │ - regs │ - regs │ - regs │ ← registers riêng │ - PC │ - PC │ - PC │ ← program counter riêng └─────────────────────────────────┘

Khi process chỉ có 1 thread, đó gọi là single-threaded process. Khi có nhiều thread, multi-threaded process. Trên Linux, thread thực ra là một loại "process" đặc biệt cùng chia sẻ address space — gọi là LWP (Lightweight Process).

3.2 Thread chia sẻ gì, không chia sẻ gì?

ResourceChia sẻ giữa các thread?
Code (text segment)✓ Chia sẻ
Data (globals, statics)✓ Chia sẻ
Heap (malloc/new)✓ Chia sẻ
File descriptors (fd table)✓ Chia sẻ
Signal handlers✓ Chia sẻ
Working directory✓ Chia sẻ
Stack✗ Riêng
Registers (gồm PC, SP)✗ Riêng
Thread ID (TID)✗ Riêng
errno✗ Riêng (TLS)
Thread-Local Storage (TLS)✗ Riêng
⚠️ Hệ quả của việc chia sẻ heap

2 thread có thể đồng thời ghi vào cùng 1 biến → race condition. Đây là nguồn gốc của 90% bug khó debug trong multi-threading. Sẽ học sâu ở Chương 6 (Synchronization).

3.3 Thread vs Process — bảng phỏng vấn

Tiêu chíProcessThread
Tạo (cost)Đắt — kernel phải copy/setup PCB, page tableRẻ — chỉ tạo stack mới
MemoryRiêng (cách ly hoàn toàn)Chia sẻ heap, code, data
Context switchĐắt — đổi page table, flush TLBRẻ — cùng address space
Communication (IPC)Phải qua pipe/socket/shared memĐọc/ghi biến chia sẻ trực tiếp
Crash isolation1 process crash không ảnh hưởng process khác1 thread crash → cả process chết
Sync overheadÍt cần (đã cô lập)Nhiều — race condition phải lock
DebuggingDễ — state riêngKhó — race, deadlock

Khi nào nên dùng cái gì?

  • Multi-process: khi cần cô lập (vd Chrome tab — crash 1 tab không sập browser); khi tính song song không cần chia sẻ data; khi muốn restart từng phần.
  • Multi-thread: khi cần chia sẻ data nhiều (vd web server xử lý nhiều request cùng struct lớn); khi tạo/destroy thường xuyên.

Ngày nay, cluster + thread pool (Node.js cluster + worker_threads) hoặc process pool (Python multiprocessing) là pattern phổ biến: cluster cho cô lập, thread cho song song trong mỗi cluster.

3.4 User-level vs Kernel-level Thread

Kernel-level Thread

  • OS biết về thread, scheduler quản lý từng thread
  • Tạo bằng syscall (clone trên Linux)
  • Mỗi thread có TID unique trên hệ thống
  • Block 1 thread không block thread khác
  • Có thể tận dụng nhiều CPU core

User-level Thread

  • OS chỉ thấy 1 thread; scheduler ở user space (thư viện) tự manage
  • Tạo cực nhanh — không cần syscall
  • Block 1 user thread = block toàn bộ process (vì OS thấy 1 thread)
  • Không tự động tận dụng multi-core
  • Vd: green thread ở Erlang, fibers, coroutines (cũ)

OS hiện đại (Linux, macOS, Windows) dùng kernel-level thread cho thread API tiêu chuẩn (pthread).

3.5 Threading Models — M:1, 1:1, M:N

3 mô hình ánh xạ user thread → kernel thread:

3.5.1 Many-to-One (M:1)

Nhiều user thread map về một kernel thread.

  • Tạo thread cực rẻ (toàn user space)
  • Nhược: 1 thread block I/O → tất cả block; không multi-core
  • Ngày nay ít dùng

3.5.2 One-to-One (1:1)

Mỗi user thread map về một kernel thread.

  • Concurrency tốt, multi-core OK
  • Nhược: tạo thread đắt (mỗi thread cần kernel resource)
  • Linux pthreads, Windows threads, Java thread đều 1:1
  • Đây là model phổ biến nhất hiện nay

3.5.3 Many-to-Many (M:N)

M user thread map về N kernel thread (M ≥ N).

  • Linh hoạt: tạo nhiều user thread, OS chỉ schedule N kernel thread
  • Phức tạp implement
  • Vd: Go goroutines dùng M:N (M goroutine, N OS thread). Đây là lý do Go cho phép tạo hàng triệu goroutine.
M:1 1:1 M:N ┌──┬──┬──┐ ┌──┬──┬──┐ ┌──┬──┬──┬──┬──┐ │U1│U2│U3│ user │U1│U2│U3│ │U1│U2│U3│U4│U5│ └─┬┴─┬┴─┬┘ └─┬┴─┬┴─┬┘ └─┬┴─┬┴─┬┴─┬┴─┬┘ └──┴──┘ │ │ │ │ │ │ │ │ │ │ │ │ └──┴──┴──┴──┘ ▼ ▼ ▼ ▼ │ │ ┌──┐ ┌──┬──┬──┐ ┌──┴──┐ │K1│ kernel │K1│K2│K3│ │K1 K2│ └──┘ └──┴──┴──┘ └─────┘

3.6 Thread Pool — pattern thực tế nhất

Tạo thread không miễn phí. Nếu mỗi request HTTP tạo 1 thread mới rồi destroy, server đông user sẽ chết. Thread Pool giải quyết: tạo sẵn N thread, request đến → đẩy vào queue, thread nhàn rỗi nhặt việc.

┌──────────────┐ request ───▶ │ Task Queue │ └──────┬───────┘ │ ┌───────────────┴───────────────┐ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌────────┐ │Worker 1│ │Worker 2│ │Worker N│ thread pool └────────┘ └────────┘ └────────┘ (kích thước cố định) │ │ │ └───────────────┼───────────────┘ ▼ Database / I/O

Lợi ích

  • Tái sử dụng thread → không tốn create/destroy
  • Giới hạn concurrency → không quá tải máy
  • Predictable resource usage

Ai dùng thread pool?

  • Java: ExecutorService, ThreadPoolExecutor
  • Tomcat: mỗi HTTP request → thread pool
  • Database connection pool: cùng concept với thread pool
  • Node.js libuv: default 4 thread cho file I/O blocking
  • nginx: dùng worker process pool, mỗi worker là event-loop

Quy tắc kinh nghiệm: kích thước pool = số CPU core × (1 + waitTime/computeTime). Web server I/O-heavy: 2-4× cores. CPU-heavy: 1× cores.

3.7 Node.js Event Loop — single-threaded mà vẫn nhanh

Câu hỏi phỏng vấn JavaScript backend cực kinh điển: "Node.js single-threaded, làm sao xử lý hàng nghìn request đồng thời?"

Câu trả lời nằm ở Event Loop + libuv.

┌─────────────────────────────────────────────────────┐ │ JavaScript Code (single thread) │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ Event Loop │ │ │ │ │ │ │ │ while (queue not empty) { │ │ │ │ task = queue.pop() │ │ │ │ execute(task) │ │ │ │ } │ │ │ └─────────────────────────────────────┘ │ │ │ delegates │ │ ▼ │ │ ┌─────────────────────────────────────┐ │ │ │ libuv (C library) │ │ │ │ ┌───────┐ ┌───────┐ ┌─────────┐ │ │ │ │ │ epoll │ │Threads│ │ DNS, FS │ │ │ │ │ │ kqueue│ │ pool │ │ ... │ │ │ │ │ └───────┘ └───────┘ └─────────┘ │ │ │ └─────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘

Cách Node xử lý 1000 request đồng thời

const http = require('http');

http.createServer((req, res) => {
  // Mỗi request KHÔNG tạo thread mới
  // Code chạy trên main thread JS
  fs.readFile('/big.json', (err, data) => {
    res.end(data);
  });
}).listen(3000);

Khi 1000 request đến cùng lúc:

  1. Main thread nhận từng request, gọi fs.readFile
  2. libuv đẩy 4 lệnh đọc file vào thread pool (default 4 workers); 996 còn lại vào queue
  3. Main thread không đợi — quay lại nhận request khác
  4. Khi worker đọc xong file, libuv push callback vào event loop queue
  5. Main thread chạy callback, gửi response, tiếp tục

6 phase của Event Loop

   ┌───────────────────────┐
┌─▶│        timers         │  ← setTimeout, setInterval
│  └──────────┬────────────┘
│  ┌──────────▼────────────┐
│  │   pending callbacks   │  ← syscall errors
│  └──────────┬────────────┘
│  ┌──────────▼────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘
│  ┌──────────▼────────────┐
│  │         poll          │  ← I/O callbacks (đa số việc ở đây)
│  └──────────┬────────────┘
│  ┌──────────▼────────────┐
│  │        check          │  ← setImmediate
│  └──────────┬────────────┘
│  ┌──────────▼────────────┐
└──│     close callbacks   │  ← socket.on('close')
   └───────────────────────┘

process.nextTick & Promise microtasks chạy giữa mỗi phase, ưu tiên cao nhất.

⚠️ "Don't block the event loop"
Nếu callback có loop tính toán nặng (vd nén ảnh 5 giây), main thread bị chặn 5s. Mọi request khác phải đợi. Giải pháp: chuyển CPU-bound work sang Worker Threads (xem 3.8) hoặc child_process.

3.8 Worker Threads — Node có thread thật

Từ Node 10.5+, có module worker_threads: tạo thread thật (pthread) để chạy CPU-bound work song song với main thread.

// main.js
const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename);
  worker.on('message', (result) => {
    console.log('Result:', result);
  });
  worker.postMessage({ n: 40 });
} else {
  // Code chạy trong worker thread
  parentPort.on('message', ({ n }) => {
    // CPU-bound work — fib(40) khá chậm
    function fib(x) { return x < 2 ? x : fib(x-1) + fib(x-2); }
    parentPort.postMessage(fib(n));
  });
}

Worker Threads vs Cluster vs child_process

APIBản chấtMemoryKhi nào dùng
worker_threadsThread thật trong cùng processChia sẻ ArrayBufferCPU-bound work, chia sẻ data lớn
clusterFork process, share port HTTPRiêngScale HTTP server qua nhiều core
child_process.forkProcess Node mớiRiêngChạy script Node độc lập, IPC
child_process.spawnProcess bất kỳRiêngChạy command shell

3.9 Concurrency vs Parallelism — phải phân biệt

Concurrency

Nhiều task đan xen trên ít CPU. Ảo giác chạy "cùng lúc" nhưng thật ra là chuyển qua chuyển lại nhanh.

Ví dụ: Node.js single thread xử lý 1000 request bằng event loop.

Câu hỏi giải quyết: "Cấu trúc app như nào để xử lý nhiều task không bị block?"

Parallelism

Nhiều task thật sự chạy cùng lúc trên nhiều CPU core.

Ví dụ: 4 thread tính toán trên 4 core khác nhau.

Câu hỏi giải quyết: "Làm sao chia 1 task lớn thành nhiều phần để giảm thời gian?"

Quote nổi tiếng của Rob Pike: "Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once."

Vẽ ra cho rõ

Concurrency (1 CPU, đan xen): CPU 1: │A1│B1│A2│C1│B2│A3│C2│B3│ (3 task A,B,C đan xen) Parallelism (3 CPU, thật song song): CPU 1: │A1│A2│A3│ CPU 2: │B1│B2│B3│ CPU 3: │C1│C2│C3│

Một system có thể vừa concurrent vừa parallel: vd Node.js cluster — concurrency trong từng process (event loop), parallelism giữa các process trên các core.

Bài tập

Bài 1 — Đếm thread của process

Trên Linux, lệnh nào liệt kê thread của 1 PID? ps -eLf | grep PID hoặc ls /proc/PID/task/.

Bài 2 — Block event loop

Viết Node script với setTimeout(() => console.log('A'), 0) và một loop nặng (vd for (i < 1e9) {}) ngay sau. Quan sát "A" được in khi nào. Giải thích.

Bài 3 — Worker thread fibonacci

Cài đặt fib(45) trong worker thread. So sánh CPU usage với main thread.

Bài 4 — UV_THREADPOOL_SIZE

Mặc định Node libuv có 4 thread cho I/O. Đặt env UV_THREADPOOL_SIZE=8. Khi nào nên tăng?

Bài 5 — Cluster vs Worker Threads

Viết 1 web server đơn giản dùng cluster (4 worker). So sánh với 1 process dùng worker_threads. Khi nào nên dùng cái nào?

Bài 6 — Concurrency vs Parallelism

Cho 4 task: tải 4 image từ network và xử lý mỗi image. Trên 4-core CPU. Bạn cần concurrency, parallelism, hay cả hai? Tại sao?

🧪 Quiz cuối chương

Câu 1. Hai thread trong cùng process chia sẻ gì?

  • Chỉ stack
  • Chỉ registers và program counter
  • Heap, code, data, file descriptors — KHÔNG chia sẻ stack và registers
  • Mọi thứ

Đáp án: heap, code, data, fd. Mỗi thread có stack riêng và registers riêng. Heap chia sẻ → race condition.

Câu 2. Tạo thread nhanh hơn tạo process vì?

  • Thread không cần tạo address space mới — dùng chung của process
  • Thread chỉ cần 1 byte memory
  • OS không phải tạo PCB cho thread
  • Process đắt vì cần chip riêng

Đáp án: dùng chung address space. fork process phải copy page table, set up new PCB. Thread chỉ cần stack mới.

Câu 3. Node.js xử lý 1000 request đồng thời nhờ?

  • Tạo 1000 thread
  • Event Loop + libuv (delegate I/O cho thread pool / OS), main thread không bao giờ block
  • Pre-fork 1000 process
  • Cluster mode bắt buộc

Đáp án: Event Loop + libuv. JS chạy single-threaded, nhưng I/O được delegate. Main thread chỉ chạy JS code khi có callback ready.

Câu 4. Concurrency vs Parallelism khác nhau ở đâu?

  • Cả hai giống nhau
  • Concurrency cần nhiều CPU, parallelism không
  • Concurrency = đan xen task (có thể 1 CPU); Parallelism = thật sự song song (cần nhiều CPU)
  • Parallelism chỉ có ở Java

Đáp án: đan xen vs song song thật. Concurrency là về structure, parallelism là về execution.

Câu 5. Linux dùng threading model nào cho pthreads?

  • 1:1 (mỗi user thread = 1 kernel thread)
  • M:1
  • M:N
  • 0:0

Đáp án: 1:1. Linux NPTL (Native POSIX Thread Library) dùng 1:1. Go dùng M:N với goroutine.

Câu 6. Khi 1 thread crash, chuyện gì xảy ra với các thread khác trong process?

  • Các thread khác tiếp tục bình thường
  • Chỉ thread crash bị kill
  • Toàn bộ process crash — vì các thread chia sẻ address space
  • OS tự restart thread

Đáp án: cả process crash. Vì memory chia sẻ, 1 thread truy cập invalid memory → segfault → kernel kill cả process. Đó là lý do Chrome dùng multi-process thay vì multi-thread cho tab.

Câu 7. CPU-bound work trong Node nên chạy ở đâu?

  • Main thread
  • Worker Threads (worker_threads module)
  • Sleep loop
  • setTimeout

Đáp án: Worker Threads. Main thread chạy CPU-bound sẽ block event loop → mọi request khác phải đợi.

Câu 8. Thread Pool có lợi gì?

  • Tái sử dụng thread, giới hạn concurrency, predictable resource
  • Tăng số CPU core
  • Giảm memory leak
  • Bảo mật thread

Đáp án: tái sử dụng + giới hạn. Tránh tạo thread mỗi request, tránh quá tải khi traffic cao.

Tổng kết chương 3

  • ✅ Thread = lightweight process; chia sẻ heap/code/data/fd, riêng stack/registers/TLS
  • ✅ Process & Thread đánh đổi: cô lập vs hiệu năng; an toàn vs đồng bộ phức tạp
  • ✅ Threading models: 1:1 (Linux pthreads), M:1 (cũ), M:N (Go goroutines)
  • ✅ Thread Pool = pattern tái sử dụng thread, giới hạn concurrency
  • Node.js Event Loop: single-threaded JS + libuv thread pool cho I/O — xử lý concurrent dù 1 thread
  • Worker Threads: thread thật trong Node cho CPU-bound work
  • Concurrency (đan xen) ≠ Parallelism (song song thật)
  • ✅ Crash 1 thread = crash cả process (chia sẻ address space)
← Chương trước Chương 02: Process & PCB Chương kế tiếp Chương 04: CPU Scheduling →