Thread vs process — chia sẻ gì, riêng gì, chi phí tạo
Thread là gì, vì sao thread cùng process chia sẻ heap nhưng mỗi thread có stack riêng, chi phí tạo thread vs process, và khi nào chọn cái nào.
TL;DR: Thread là một luồng thực thi bên trong một tiến trình. Nhiều thread trong cùng một process chia sẻ không gian địa chỉ: cùng code, cùng dữ liệu toàn cục, cùng heap, cùng bảng file descriptor. Cái mỗi thread giữ riêng là thứ cần để chạy độc lập: stack riêng (biến cục bộ, chuỗi lời gọi hàm) và bộ register riêng (gồm program counter và stack pointer). Vì chia sẻ address space, tạo một thread rẻ hơn tạo một process khoảng một bậc, và giao tiếp giữa các thread chỉ là đọc/ghi cùng biến. Process cho cách ly (một process sập không kéo process khác chết); thread cho song song trên bộ nhớ chung với chi phí thấp.
Máy chủ của bạn nhận 10.000 kết nối cùng lúc. Mô hình cũ (Apache prefork) tạo một tiến trình cho mỗi kết nối: 10.000 process, mỗi cái một bản sao không gian địa chỉ, mỗi cái tốn vài trăm KB tới hàng MB bộ nhớ kernel cho page table và cấu trúc quản lý. Máy hết RAM trước khi hết CPU. Mô hình mới tạo một thread cho mỗi kết nối trong cùng một process: 10.000 thread chia sẻ một address space, dùng chung cache dữ liệu, tạo nhanh hơn nhiều. Cùng một bài toán, khác nhau ở một quyết định: dùng process hay thread.
Bài này giải thích thread khác process ở đâu — chính xác cái gì chia sẻ, cái gì riêng, vì sao thiết kế như vậy, và khi nào bạn nên chọn cái nào. Học xong, bạn Compare được thread và process trên ba trục — chia sẻ gì, riêng gì, chi phí tạo — và biết chọn cái nào cho một bài toán cụ thể. Đây là nền cho mọi bài còn lại trong module: context switch, scheduler, và chọn số thread đều xây trên hiểu biết này.
1. Analogy — công ty và nhân viên chung một văn phòng
Hình dung một process là một công ty thuê nguyên một tầng văn phòng. Thread là các nhân viên làm việc trong công ty đó.
Mọi nhân viên dùng chung cơ sở vật chất của tầng: cùng kho tài liệu (heap), cùng bảng thông báo dán ngoài sảnh (dữ liệu toàn cục), cùng danh bạ điện thoại của công ty (bảng file descriptor). Ai cũng đọc và sửa được kho tài liệu chung — tiện, nhưng nếu hai người cùng sửa một hồ sơ mà không hẹn nhau thì hồ sơ hỏng.
Nhưng mỗi nhân viên có bàn làm việc riêng: giấy nháp đang viết dở (biến cục bộ), danh sách việc đang làm theo thứ tự (chuỗi lời gọi hàm), và bút đang cầm trên tay chỉ vào dòng đang đọc (program counter). Không ai ngồi chung bàn được, vì ai cũng đang làm một mạch việc riêng.
Một process khác là một công ty khác thuê tầng khác. Hai công ty không thấy kho tài liệu của nhau; muốn trao đổi phải gửi thư ra ngoài (IPC — bài của module 04). Công ty này cháy thì công ty kia vẫn an toàn.
| Đời thường | Khái niệm |
|---|---|
| Công ty thuê một tầng | Process (một không gian địa chỉ) |
| Nhân viên trong công ty | Thread |
| Kho tài liệu chung, bảng thông báo | Heap + dữ liệu toàn cục (chia sẻ) |
| Danh bạ điện thoại công ty | Bảng file descriptor (chia sẻ) |
| Bàn làm việc riêng, giấy nháp, bút chỉ dòng | Stack + register riêng mỗi thread |
| Công ty khác thuê tầng khác | Process khác (address space riêng) |
| Gửi thư giữa hai công ty | IPC giữa hai process |
Thread chia sẻ tài nguyên của process (code, heap, file), nhưng giữ riêng trạng thái thực thi (stack, register). Câu một dòng: "chung nhà, riêng bàn".
2. Chia sẻ gì, riêng gì — theo đúng chuẩn POSIX
Trước hết định nghĩa cho chặt. Một process là một chương trình đang chạy, sở hữu một không gian địa chỉ ảo (virtual address space) riêng — vùng nhớ mà chỉ nó thấy (khoá Bộ nhớ, bài bộ nhớ ảo). Một thread (POSIX gọi là pthread) là một luồng thực thi bên trong một process. Một process luôn có ít nhất một thread (thread main); nó có thể tạo thêm nhiều thread nữa, tất cả sống trong cùng address space đó.
Chuẩn POSIX pthreads(7) liệt kê rõ cái gì chia sẻ giữa các thread cùng process và cái gì riêng:
| Chia sẻ (chung cả process) | Riêng mỗi thread |
|---|---|
| Code (text segment) | Thread ID |
| Dữ liệu toàn cục + heap | Stack (biến cục bộ) |
| Bảng file descriptor (file đang mở) | Bộ register (gồm program counter, stack pointer) |
| Signal disposition (handler cho mỗi signal) | Signal mask (signal nào đang chặn) |
Thư mục hiện tại, umask, nice value | errno |
| Process ID, parent PID | Alternate signal stack, CPU affinity |
Lưu ý nhỏ về
nice value: chuẩn POSIX xếp nó vào nhóm chia sẻ cả process, nhưng Linux hiện thực nó per-thread — mỗi thread có nice riêng. Nếu bạn dựa vào nice để chỉnh ưu tiên trên Linux, nhớ nó tác động từng thread, không phải cả process. Chi tiết ở phần NOTES của pthreads(7).
Điểm mấu chốt để nhớ: thứ thuộc về "chương trình" thì chung; thứ thuộc về "một luồng đang chạy tới đâu" thì riêng. Code là của chương trình nên chung. Heap chứa object dùng chung nên chung. Nhưng "tôi đang thực thi dòng nào, gọi hàm nào, biến cục bộ ra sao" là của riêng từng luồng — nên stack và register phải riêng.
Sơ đồ bố cục bộ nhớ của một process có hai thread — nối thẳng với mô hình stack/heap bạn đã học ở khoá Bộ nhớ:
Chú ý: chỉ có một heap, một vùng code — dùng chung. Nhưng có hai stack, mỗi thread một cái. Đây là hình ảnh trung tâm của cả bài.
3. Cơ chế bên dưới — vì sao stack và register phải riêng
Câu hỏi hay: tại sao bắt buộc mỗi thread phải có stack và register riêng, trong khi heap thì chung được?
Một CPU core tại một thời điểm đang thực thi đúng một luồng lệnh. Để "biết mình đang ở đâu", nó dùng vài register đặc biệt:
- Program counter (PC) — địa chỉ lệnh kế tiếp sẽ chạy.
- Stack pointer (SP) — đỉnh stack hiện tại, tức khung hàm đang hoạt động.
- Các register dữ liệu khác đang giữ giá trị tính toán dở.
Nếu hai thread cùng một luồng lệnh nhưng đang ở hai chỗ khác nhau (thread 1 đang trong parseRequest(), thread 2 đang trong writeResponse()), chúng phải có hai bộ PC/SP khác nhau. Chung một PC là vô lý — không thể vừa ở dòng này vừa ở dòng kia. Vì vậy mỗi thread giữ riêng bộ register.
Stack cũng vậy. Stack lưu biến cục bộ và khung của từng lời gọi hàm (stack frame ở khoá Bộ nhớ). Hai thread gọi hàm độc lập nhau, chuỗi lời gọi khác nhau, biến cục bộ khác nhau — nên mỗi thread cần một stack riêng. Nếu dùng chung stack, lời gọi hàm của thread này sẽ giẫm lên khung của thread kia.
Ngược lại, heap chứa object "của chương trình", không gắn với một luồng cụ thể. Thread 1 tạo một object trên heap, thread 2 hoàn toàn có thể đọc nó — đó chính là cách hai thread chia sẻ dữ liệu. Không có lý do bắt heap phải riêng, và làm riêng thì mất luôn khả năng chia sẻ rẻ tiền.
flowchart TB
subgraph P["Process — 1 address space"]
CODE["Code + Heap + Data<br/>(chia se)"]
subgraph T1["Thread 1"]
R1["PC, SP, registers 1"]
S1["Stack 1"]
end
subgraph T2["Thread 2"]
R2["PC, SP, registers 2"]
S2["Stack 2"]
end
end
T1 --> CODE
T2 --> CODETrên Linux, sự chia sẻ này không phải phép màu — nó là tham số của một syscall. Tạo process dùng fork() (bản sao address space); tạo thread dùng clone() với cờ CLONE_VM (dùng chung không gian nhớ), CLONE_FILES (dùng chung bảng fd), CLONE_FS... Thư viện pthread gọi clone() với đúng bộ cờ này. Nói cách khác, với kernel Linux, thread chỉ là "một tiến trình chia sẻ gần hết mọi thứ với anh em của nó".
4. Chi phí tạo: thread rẻ hơn process khoảng một bậc
Vì thread không phải dựng một address space mới, tạo thread rẻ hơn tạo process đáng kể.
Khi fork() một process, kernel phải lập một không gian địa chỉ mới: sao chép bảng ánh xạ trang (page table), thiết lập cấu trúc quản lý bộ nhớ, sao chép bảng fd... Linux dùng copy-on-write để không sao chép ngay từng trang dữ liệu (chỉ sao khi bị ghi), nên fork() không quá đắt, nhưng vẫn phải dựng và sao chép page table — công việc tỉ lệ với kích thước không gian địa chỉ.
Khi tạo thread, kernel chỉ cần: cấp một stack mới, tạo một cấu trúc mô tả thread, và đăng ký nó với scheduler. Không dựng address space, không sao page table.
Kết quả thực nghiệm (đo bằng lmbench hoặc microbenchmark tương tự trên Linux x86-64 phổ thông) cho biên độ như sau — coi là bậc độ lớn, không phải hằng số tuyệt đối vì phụ thuộc CPU, kernel, kích thước process:
| Thao tác | Biên độ điển hình |
|---|---|
Tạo + huỷ một thread (pthread_create) | Hàng chục µs |
Tạo + huỷ một process (fork + exit) | Hàng trăm µs tới ~1 ms cho tiến trình phổ thông (nặng hơn nếu address space lớn) |
Tức tạo thread thường rẻ hơn tạo process khoảng một bậc (chục lần), không phải nghìn lần. Con số cụ thể trên máy bạn sẽ khác; điều cần nhớ là hướng và biên độ: thread nhẹ hơn process một cách có hệ thống, và khoảng cách lớn dần khi process có address space lớn (fork phải sao nhiều page table hơn).
Rẻ khi tạo không có nghĩa là chạy nhanh hơn. Khi chạy, cả thread và process đều được scheduler cấp CPU như nhau. Thread có thêm cái lợi lúc context switch giữa hai thread cùng process: không phải đổi address space nên không flush TLB — bài 02 sẽ mổ kỹ. "Thread luôn nhanh hơn process" là hiểu sai; đúng hơn là "thread rẻ hơn khi tạo và khi chuyển đổi trong cùng process".
5. Khi nào chọn process, khi nào chọn thread?
Không có bên nào thắng tuyệt đối — chọn theo bài toán.
| Tình huống | Nên dùng | Vì sao |
|---|---|---|
| Cần cách ly lỗi: một phần sập không kéo phần khác chết | Process | Mỗi process address space riêng; crash một cái, cái khác vẫn sống |
| Cần cách ly bảo mật giữa các thành phần | Process | Ranh giới bộ nhớ do kernel enforce; thread cùng process thấy hết bộ nhớ của nhau |
| Song song trên cùng dữ liệu lớn (xử lý một mảng, một cache chung) | Thread | Chia sẻ heap: không phải copy dữ liệu qua lại, giao tiếp gần như miễn phí |
| Nhiều tác vụ nhẹ, tạo/huỷ liên tục | Thread | Tạo rẻ hơn một bậc; process pool đắt hơn nhiều |
| Chạy chương trình khác hẳn (trình con, plugin không tin cậy) | Process | fork + exec để nạp binary khác; sandbox được |
Ví dụ thực tế: trình duyệt Chrome cho mỗi tab thường một renderer process riêng (chính sách Site Isolation; Chrome có thể gộp nhiều tab vào chung một process khi máy thiếu RAM) để một tab treo hay bị khai thác không hạ cả trình duyệt và không đọc được dữ liệu tab khác — đổi lấy chi phí bộ nhớ cao hơn. Ngược lại, một web server Java xử lý request bằng thread pool vì các request chia sẻ cache, connection pool, config trong cùng heap, và cần tạo/thu hồi nhanh.
6. Pitfall của riêng concept này
❌ Nhầm 1 — tưởng mỗi thread có heap riêng:
// Thread A
sharedList.add("x"); // sharedList o tren HEAP CHUNG
// Thread B doc cung luc
int n = sharedList.size(); // thay thay doi cua A ngay -- CHUNG HEAP
✅ Chỉ có một heap cho cả process. sharedList mà cả hai thread cùng tham chiếu là một object duy nhất — B thấy thay đổi của A. Đây vừa là sức mạnh (chia sẻ rẻ) vừa là bẫy: nếu A và B cùng add không đồng bộ, cấu trúc nội bộ của list hỏng. Đồng bộ hoá là chủ đề module 04.
❌ Nhầm 2 — tưởng biến cục bộ cũng bị chia sẻ:
void handle() {
int count = 0; // bien cuc bo -> nam tren STACK RIENG cua moi thread
count++; // moi thread co ban 'count' rieng, khong dam nhau
}
✅ Biến cục bộ nằm trên stack, mà stack thì riêng mỗi thread. Hai thread cùng chạy handle() có hai biến count độc lập — không cần đồng bộ. Quy tắc: dữ liệu trên stack (cục bộ) thì an toàn tự nhiên; dữ liệu trên heap (chia sẻ) mới cần đồng bộ.
❌ Nhầm 3 — tưởng một thread crash chỉ chết thread đó:
✅ Một lỗi nghiêm trọng không bắt được (ví dụ segfault do thao tác bộ nhớ sai trong native code) làm sập cả process — kéo theo mọi thread. Vì mọi thread chung một address space, hỏng bộ nhớ ở một thread có thể phá dữ liệu của thread khác. Đây chính là lý do khi cần cách ly lỗi thật sự, người ta chọn process, không phải thread.
7. 📚 Deep Dive Linux
Spec / man page chính thức:
- pthreads(7) — man page liệt kê đầy đủ thuộc tính chia sẻ và riêng giữa các thread POSIX (nguồn của bảng ở mục 2). Đọc phần "the following are shared" và "distinct attributes".
- clone(2) — syscall tạo luồng thực thi trên Linux; các cờ
CLONE_VM,CLONE_FILES,CLONE_FSchính là công tắc bật/tắt việc chia sẻ. Thread =clonevới gần hết cờ bật; process =fork(cơ bản không bật cờ chia sẻ). - fork(2) — tạo process con bằng bản sao (copy-on-write) address space cha.
Ghi chú: trên Linux, thread và process không phải hai cơ chế tách biệt — cả hai đều là "task" với các mức chia sẻ khác nhau, đều do clone() tạo ra. Hiểu điều này giúp bạn thôi coi thread là "phép màu": nó chỉ là một task chia sẻ address space với anh em.
8. Liên hệ các bài khác
- Bài 02 — Trạng thái và context switch: vì thread chia sẻ address space, context switch giữa hai thread cùng process rẻ hơn giữa hai process (không flush TLB) — bài sau đo cụ thể.
- Tiến trình và PCB (Module 02): process là gì và kernel lưu trạng thái nó ở đâu — nền để hiểu thread là "task chia sẻ".
- Stack và stack frame (khoá Bộ nhớ): vì sao mỗi thread cần stack riêng — mỗi stack là một chuỗi khung hàm độc lập.
- Race condition (Module 04): mặt trái của chia sẻ heap — hai thread ghi cùng dữ liệu gây hỏng, và cách đồng bộ.
9. Tóm tắt
- Thread là luồng thực thi trong một process; một process có ít nhất một thread và có thể tạo thêm.
- Thread cùng process chia sẻ code, dữ liệu toàn cục, heap, bảng file descriptor — mọi thứ thuộc về "chương trình".
- Mỗi thread giữ riêng stack và bộ register (gồm program counter và stack pointer) — mọi thứ thuộc về "một luồng đang chạy tới đâu".
- Stack/register phải riêng vì một CPU core chỉ ở được một chỗ tại một thời điểm; heap chung được vì object không gắn với luồng cụ thể.
- Tạo thread rẻ hơn tạo process khoảng một bậc (chục µs so với trăm µs tới ~1 ms) vì không phải dựng và sao chép address space.
- Chọn process khi cần cách ly lỗi/bảo mật; chọn thread khi cần song song rẻ trên bộ nhớ chung.
- Chia sẻ heap khiến giao tiếp rẻ nhưng đòi hỏi đồng bộ; một thread làm sập bộ nhớ có thể kéo cả process chết.
10. Tự kiểm tra
Q1Vì sao thread chia sẻ được heap nhưng PHẢI có stack riêng? Điều gì vỡ nếu hai thread dùng chung một stack?▸
Q2Vì sao mỗi thread bắt buộc phải có program counter và stack riêng, trong khi heap thì chia sẻ được?▸
parseRequest, thread 2 trong writeResponse) — không thể dùng chung một program counter vì không thể vừa ở dòng này vừa ở dòng kia. Stack cũng phải riêng vì mỗi thread có chuỗi lời gọi hàm và biến cục bộ độc lập; dùng chung thì lời gọi của thread này giẫm lên khung của thread kia. Ngược lại, heap chứa object "của chương trình", không gắn với luồng nào cụ thể — thread 1 tạo object, thread 2 đọc được, đó chính là cách chia sẻ dữ liệu. Bắt heap riêng thì mất luôn lợi ích chia sẻ mà chẳng được gì.Q3Vì sao tạo một thread rẻ hơn tạo một process? Chênh lệch cỡ bao nhiêu, và điều gì làm chênh lệch đó lớn hơn?▸
fork) phải dựng một không gian địa chỉ mới: lập và sao chép page table, thiết lập cấu trúc quản lý bộ nhớ, sao bảng file descriptor. Linux dùng copy-on-write nên không sao ngay dữ liệu, nhưng vẫn phải sao page table — công việc tỉ lệ với kích thước address space. Tạo thread (clone với CLONE_VM) không dựng address space: chỉ cấp một stack, tạo cấu trúc mô tả thread, đăng ký với scheduler. Chênh lệch điển hình khoảng một bậc — thread hàng chục µs, process hàng trăm µs tới ~1 ms (coi là biên độ, không phải hằng số). Chênh lệch lớn hơn khi process có address space lớn: fork phải sao nhiều page table hơn, còn tạo thread thì gần như không đổi.Q4Chrome dùng một process cho mỗi tab, còn một web server Java dùng thread pool. Giải thích vì sao mỗi bên chọn khác nhau.▸
Q5Trong một hàm xử lý request chạy bởi nhiều thread, biến cục bộ int count và một ArrayList trên heap dùng chung — cái nào cần đồng bộ, cái nào không? Vì sao?▸
count không cần đồng bộ: nó nằm trên stack, mà mỗi thread có stack riêng. Hai thread cùng chạy hàm đó có hai bản count hoàn toàn độc lập, không thể giẫm nhau. ArrayList dùng chung trên heap thì cần đồng bộ: chỉ có một object duy nhất mà mọi thread cùng tham chiếu, nên hai thread cùng add đồng thời có thể làm hỏng cấu trúc nội bộ (mảng backing, biến size) — kinh điển của race condition. Quy tắc rút ra: dữ liệu trên stack (cục bộ) an toàn tự nhiên vì riêng từng thread; dữ liệu trên heap (chia sẻ) mới cần cơ chế đồng bộ. Đây là lý do một hàm chỉ dùng biến cục bộ thường "thread-safe" mà không cần làm gì thêm.Q6Một đồng nghiệp nói 'cứ dùng thread thay process đi cho nhanh, thread luôn nhanh hơn'. Câu này sai ở đâu?▸
Bài tiếp theo: Ready, running, blocked — và giá của context switch
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