Tổng kết module — Thread & Lập lịch CPU
Cheat sheet thread vs process + trạng thái + scheduler, glossary, pitfall tổng hợp và self-assessment đối chiếu learning outcomes.
TL;DR: Module này trả lời câu hỏi "4 core chạy 400 tiến trình thế nào" bằng bốn mảnh ghép. Thread khác process ở chỗ chia sẻ address space (chung heap/code, riêng stack/register) — nên tạo và chuyển đổi rẻ hơn. Một thread luân phiên ready/running/blocked, và blocked thì nhường CPU chứ không phí. Mỗi lần đổi thread là một context switch tốn cả phần trực tiếp (register) lẫn gián tiếp (cache lạnh, TLB flush). Scheduler preemptive dùng timer interrupt chia core theo time slice, priority/nice và ý tưởng công bằng của CFS. Cuối cùng, phân loại workload CPU-bound vs I/O-bound cho bạn công thức chọn số thread. Đây là trang để bookmark.
Đã đi qua những gì
Module bắt đầu từ thread vs process: thread cùng process chia sẻ code, dữ liệu toàn cục, heap và bảng file descriptor, nhưng mỗi thread giữ riêng stack và bộ register (gồm program counter, stack pointer) — vì đó là thứ tối thiểu để một luồng chạy độc lập. Chia sẻ address space làm tạo thread rẻ hơn tạo process khoảng một bậc, và giao tiếp giữa thread gần như miễn phí — đổi lại phải đồng bộ khi ghi chung, và mất khả năng cách ly lỗi.
Rồi bạn đưa thread vào chuyển động: ba trạng thái ready (chờ tới lượt), running (trên core), blocked (chờ sự kiện). Ý quan trọng nhất — blocked không tốn CPU — là nền cho cả module: kernel lấy thread chờ khỏi core và chạy thread khác. Mỗi lần đổi thread là một context switch, và chi phí thật của nó không chỉ là lưu/khôi phục register (vài µs) mà còn là cái đuôi gián tiếp: thread mới vào gặp cache lạnh, TLB có thể vừa bị flush — nối thẳng về cache/TLB ở khoá Bộ nhớ. Đổi thread cùng process nhẹ hơn đổi process vì không đổi address space.
Tiếp theo là scheduler: lập lịch preemptive dùng timer interrupt để cướp CPU đều đặn, nên while (true) {} không treo được máy. Time slice là đánh đổi giữa phản hồi nhanh và chi phí switch; nice (-20..+19) điều chỉnh ưu tiên; và CFS chọn thread có vruntime nhỏ nhất qua cây đỏ-đen, tự nhiên ưu tiên thread hay ngủ (I/O-bound).
Cuối cùng, mọi thứ hội tụ ở CPU-bound vs I/O-bound: CPU-bound thì số thread ≈ số core (thêm nữa chỉ tăng switch), I/O-bound thì nhiều thread hơn core để lấp thời gian chờ. Mini-challenge cho bạn tự đo hai đường cong đó trên máy mình.
flowchart TD ROOT["Thread va Lap lich CPU"] ROOT --> TP["Thread vs process"] ROOT --> ST["Trang thai va context switch"] ROOT --> SC["Scheduler"] ROOT --> WL["CPU-bound vs I/O-bound"] TP --> TPd["Chia se: code, heap, data, fd<br/>Rieng: stack, register, PC, SP<br/>Tao thread re hon ~1 bac<br/>Process de cach ly, thread de chia se"] ST --> STd["Ready / running / blocked<br/>Blocked khong ton CPU<br/>Truc tiep: luu/nap register<br/>Gian tiep: cache lanh, TLB flush"] SC --> SCd["Preemptive: timer interrupt<br/>Time slice: phan hoi vs overhead<br/>Nice -20..+19: uu tien tuong doi<br/>CFS chon vruntime nho nhat"] WL --> WLd["CPU-bound: so thread ~ so core<br/>I/O-bound: nhieu thread hon core<br/>Cong thuc: core x (1 + cho/tinh)<br/>Do that roi dieu chinh"]
🗺️ Cheat sheet
| Khái niệm | Cốt lõi | Pitfall / khi nào dùng |
|---|---|---|
| Process | Chương trình đang chạy, có address space riêng | Cách ly lỗi/bảo mật; tạo đắt hơn thread |
| Thread | Luồng thực thi trong một process | Chia sẻ heap → cần đồng bộ; một thread sập kéo cả process |
| Chia sẻ (thread) | Code, dữ liệu toàn cục, heap, bảng file descriptor | Ghi chung không đồng bộ → race condition |
| Riêng (thread) | Stack, bộ register (PC, SP), thread ID, errno | Biến cục bộ (stack) an toàn tự nhiên, không cần đồng bộ |
| Ready | Sẵn sàng chạy, chờ core rảnh | — |
| Running | Đang chiếm một core | Chỉ trạng thái này tiêu CPU |
| Blocked | Chờ sự kiện (I/O, lock), không cần CPU | Busy-wait (while quay vòng) thì vẫn đốt CPU — khác blocking |
| Context switch | Đổi thread trên một core: lưu/nạp register | Trực tiếp vài µs; gián tiếp (cache/TLB) thường lớn hơn |
| Thread switch vs process switch | Cùng process không flush TLB; khác process thì có | PCID/ASID giảm nhẹ đòn TLB nhưng không xoá |
| Preemptive scheduling | Timer interrupt cướp CPU đều đặn | while (true) {} không treo máy được |
| Time slice | Thời gian chạy trước khi preempt | Ngắn = phản hồi nhanh + tốn switch; dài = ngược lại |
| Nice value | -20 (ưu tiên cao) tới +19 (thấp); ~1,25× mỗi đơn vị | Không đặt mức CPU tuyệt đối; giới hạn cứng dùng cgroups |
| CFS | Chọn thread có vruntime nhỏ nhất (cây đỏ-đen) | Cân theo tổng thời gian chạy, không phải số lượt |
| vruntime | Thời gian ảo mỗi thread đã chạy (điều chỉnh theo nice) | Thread hay ngủ tích ít → được ưu tiên khi thức |
| CPU-bound | Gần như luôn tính, nút cổ chai là core | Số thread ≈ số core; thêm nữa gây switch, chậm hơn |
| I/O-bound | Phần lớn thời gian blocked chờ | Nhiều thread hơn core; coi chừng nút cổ chai downstream |
| Công thức số thread | số_core × (1 + chờ/tính) | Heuristic — đo thật rồi điều chỉnh |
📖 Glossary module
| Thuật ngữ | Định nghĩa 1 câu |
|---|---|
| Thread | Luồng thực thi bên trong một process; một process có ít nhất một thread |
| Process | Một chương trình đang chạy, sở hữu một không gian địa chỉ ảo riêng |
| Address space | Không gian địa chỉ ảo một process thấy; thread cùng process chia sẻ nó |
| Stack (thread) | Vùng lưu biến cục bộ và khung lời gọi hàm, riêng mỗi thread |
| Program counter | Register chỉ địa chỉ lệnh kế tiếp sẽ chạy — riêng mỗi thread |
| Context switch | Thao tác kernel đổi thread đang chạy trên một core, lưu/nạp trạng thái CPU |
| TLB | Bộ đệm ánh xạ địa chỉ ảo→vật lý; có thể bị flush khi đổi process |
| Ready state | Thread sẵn sàng chạy nhưng đang chờ được cấp một core |
| Running state | Thread đang thực thi trên một core |
| Blocked state | Thread đang chờ một sự kiện (I/O, lock), tạm thời không cần CPU |
| Busy-wait | Vòng lặp chủ động kiểm tra điều kiện — đốt CPU, khác với blocking |
| Scheduler | Phần kernel chọn thread ready nào chạy tiếp trên mỗi core |
| Preemptive scheduling | Kernel có thể lấy lại CPU khỏi thread bất cứ lúc nào (qua timer interrupt) |
| Timer interrupt | Interrupt phần cứng phát đều đặn, đưa kernel vào để preempt |
| Time slice (quantum) | Lượng thời gian một thread được chạy trước khi bị cân nhắc preempt |
| Nice value | Số (-20..+19) điều chỉnh mức ưu ái của thread với scheduler |
| CFS | Completely Fair Scheduler — scheduler Linux chọn thread vruntime nhỏ nhất |
| vruntime | Thời gian ảo tích luỹ mỗi thread đã chạy, điều chỉnh theo nice |
| CPU-bound | Workload bị giới hạn bởi tốc độ CPU; thread gần như luôn running |
| I/O-bound | Workload bị giới hạn bởi I/O; thread phần lớn thời gian blocked |
| Thread pool | Tập thread tái dùng để chạy nhiều tác vụ, tránh tạo/huỷ thread liên tục |
⚠️ Pitfall tổng hợp
1. Tưởng mỗi thread có heap riêng:
// SAI (hieu nham): tuong A va B co ban list rieng
sharedList.add("x"); // A
int n = sharedList.size(); // B thay ngay -- CHUNG HEAP
// DUNG: chi 1 heap; sharedList la 1 object duy nhat.
// Ghi chung phai dong bo (module 04). Bien cuc bo (stack) moi rieng.
2. Busy-wait thay vì blocking:
// SAI: dot 100% mot core de "cho"
while (!dataReady) { }
// DUNG: nhuong core cho toi khi co su kien
synchronized (lock) {
while (!dataReady) lock.wait(); // thread -> BLOCKED, khong ton CPU
}
3. Tưởng nice giới hạn CPU tuyệt đối:
# SAI (hieu nham): tuong nice +19 = toi da X% CPU
nice -n 19 ./job.sh # van dung 100% mot core neu khong ai khac can
# DUNG: gioi han cung bang cgroups CPU quota, khong phai nice
4. Thêm thread cho workload CPU-bound:
// SAI: 200 thread cho tac vu tinh toan tren 4 core
// -> van 4 core lam viec, + hang loat context switch -> CHAM HON
ExecutorService pool = Executors.newFixedThreadPool(200);
// DUNG: CPU-bound -> so thread ~ so core
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService pool2 = Executors.newFixedThreadPool(cores);
5. Gộp CPU-bound và I/O-bound vào một pool:
// SAI: mot con so khong toi uu cho hai ban chat trai nguoc
ExecutorService pool = Executors.newFixedThreadPool(50);
// DUNG: tach pool, chinh so thread doc lap theo loai
ExecutorService cpuPool = Executors.newFixedThreadPool(cores); // tinh nang
ExecutorService ioPool = Executors.newFixedThreadPool(cores * 5); // goi mang/DB
✅ Self-assessment
Bạn đã đạt module này nếu trả lời được:
- Compare được thread và process: cái gì chia sẻ (code, heap, dữ liệu toàn cục, bảng file descriptor), cái gì riêng (stack, register gồm PC/SP), và khi nào chọn process (cách ly) vs thread (chia sẻ rẻ).
- Nếu chưa: đọc lại bài 01 — Thread vs process mục 2–5.
- Trace được vòng đời một thread qua ba trạng thái ready/running/blocked và chi phí thật của mỗi lần context switch — cả trực tiếp (lưu/nạp register) lẫn gián tiếp (cache lạnh, TLB flush), và vì sao đổi thread cùng process rẻ hơn đổi process.
- Nếu chưa: đọc lại bài 02 — Trạng thái và context switch mục 2–6.
- Explain được scheduler preemptive chia CPU thế nào: timer interrupt cướp CPU, đánh đổi độ dài time slice, nice value, và ý tưởng CFS chọn vruntime nhỏ nhất.
- Nếu chưa: đọc lại bài 03 — Scheduler và time slice mục 2–5.
- Choose được số thread hợp lý cho workload CPU-bound (≈ số core) vs I/O-bound (nhiều hơn, theo tỉ lệ chờ/tính) dựa trên dự đoán bản chất workload — và giải thích vì sao mỗi loại như vậy.
- Nếu chưa: đọc lại bài 04 — CPU-bound vs I/O-bound mục 3–5 và làm lại bài 05 — Mini-challenge.
🚀 What's next
Module này cho bạn hiểu một thread/tiến trình chạy và được chia CPU thế nào. Nhưng khi nhiều thread cùng ghi một dữ liệu chung — chính cái heap chia sẻ ở bài 01 — mọi thứ có thể vỡ theo cách khó lường: hai thread cùng count++ mất kết quả của nhau, hai thread cùng chờ nhau nhả lock thì treo mãi mãi.
Tiếp theo — Module 04: Đồng bộ và phối hợp. Bạn sẽ học vì sao count++ không nguyên tử, race condition xảy ra thế nào, mutex và atomic sửa nó ra sao, deadlock hình thành và cách tránh, và các cơ chế IPC (pipe, shared memory, socket) để process trao đổi. Đây là nửa còn lại của bức tranh đa nhiệm: module 03 dạy chia CPU, module 04 dạy phối hợp an toàn khi chia sẻ dữ liệu.
Bắt đầu: Module 04 — Tổng quan: Đồng bộ và phối hợp
Khám phá thêm: Tất cả khoá học
📚 Tài liệu mở rộng
- OSTEP — "Operating Systems: Three Easy Pieces" (Arpaci-Dusseau), miễn phí tại https://pages.cs.wisc.edu/~remzi/OSTEP/ — các chương "The Abstraction: The Process", "Scheduling: Introduction" và "Multiprocessor Scheduling" phủ đúng module này, sâu hơn và có ví dụ đo đạc.
- pthreads(7) và clone(2) — man page chuẩn về thread POSIX và cơ chế chia sẻ trên Linux; đọc để thấy thread và process chỉ là "task" với mức chia sẻ khác nhau.
- sched(7) và Tài liệu kernel — CFS Scheduler — nguồn chuẩn về chính sách lập lịch, nice, và thiết kế CFS (kèm ghi chú CFS nhường chỗ EEVDF).
- "Java Concurrency in Practice" (Brian Goetz, 2006) — chương 8 "Applying Thread Pools" cho công thức chọn số thread và cách kích cỡ pool theo workload; nền để áp bài 04/05 vào code Java thật.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên