Process vs Thread: Điều Mỗi Lập Trình Viên Cần Biết

Mọi lập trình viên đều nghe "process" và "thread" liên tục — trong phỏng vấn, thảo luận thiết kế hệ thống và thông báo lỗi. Nhưng nhiều người coi chúng như nhau hoặc chỉ hiểu mơ hồ về sự khác biệt.
Bài viết này sẽ làm rõ sự nhầm lẫn đó. Đến cuối bài, bạn sẽ biết chính xác process và thread là gì, chúng khác nhau như thế nào về bộ nhớ và giao tiếp, và khi nào nên dùng cái nào.
Bạn Sẽ Học Được Gì
✅ Process là gì và cách OS quản lý nó
✅ Thread là gì và liên quan đến process như thế nào
✅ Bộ nhớ được chia sẻ (hay không) giữa chúng
✅ Chúng giao tiếp với nhau như thế nào
✅ Đánh đổi hiệu năng của từng loại
✅ Ví dụ code thực tế bằng Python, Go và Java
✅ Khi nào dùng process, khi nào dùng thread
Mô Hình Tư Duy
Hãy nghĩ process như một ứng dụng đang chạy. Khi bạn mở Chrome, đó là một process. Mở VS Code — một process khác. Mỗi cái tồn tại trong thế giới riêng biệt, cô lập.
Thread là đơn vị thực thi bên trong một process. Chrome có thể có một thread render trang, một thread chạy JavaScript, và một thread xử lý yêu cầu mạng — tất cả bên trong cùng một process Chrome.
Process Là Gì?
Process là một chương trình độc lập đang thực thi. Hệ điều hành cấp cho mỗi process:
- Không gian bộ nhớ ảo riêng (code, stack, heap, data)
- File descriptor riêng (file đang mở, socket)
- CPU register và program counter riêng
- Một Process ID (PID) duy nhất
Vì mỗi process có không gian bộ nhớ riêng, một process không thể vô tình đọc hoặc làm hỏng bộ nhớ của process khác. Sự cô lập này là đặc điểm chính của process.
Vòng đời của process
Một process trải qua các trạng thái sau:
| Trạng thái | Mô tả |
|---|---|
| New | Đang được tạo |
| Ready | Chờ được cấp CPU |
| Running | Đang thực thi trên CPU |
| Blocked/Waiting | Chờ I/O hoặc sự kiện |
| Terminated | Đã kết thúc thực thi |
Thread Là Gì?
Thread là đơn vị thực thi nhỏ nhất mà OS scheduler có thể quản lý. Mỗi process có ít nhất một thread — main thread. Có thể tạo thêm thread để làm việc song song.
Các thread trong cùng một process chia sẻ:
- Heap (bộ nhớ động)
- Biến toàn cục và dữ liệu tĩnh
- File descriptor đang mở
- Code (các lệnh thực thi)
Nhưng mỗi thread có riêng:
- Stack (biến cục bộ, lịch sử gọi hàm)
- Program counter (đang thực thi lệnh nào)
- CPU register
| Thành phần | Thread 1 | Thread 2 | Thread 3 |
|---|---|---|---|
| Heap (dùng chung) | ✅ | ✅ | ✅ |
| Code (dùng chung) | ✅ | ✅ | ✅ |
| Biến toàn cục (dùng chung) | ✅ | ✅ | ✅ |
| Stack (riêng) | Stack 1 | Stack 2 | Stack 3 |
| Program Counter (riêng) | PC 1 | PC 2 | PC 3 |
So Sánh Các Điểm Khác Biệt Chính
| Khía cạnh | Process | Thread |
|---|---|---|
| Bộ nhớ | Cô lập — không gian địa chỉ riêng | Chia sẻ trong process |
| Chi phí tạo | Cao (OS phải thiết lập bộ nhớ) | Thấp (dùng chung bộ nhớ cha) |
| Context switch | Tốn kém (lưu/khôi phục toàn bộ trạng thái) | Rẻ hơn (chia sẻ phần lớn trạng thái) |
| Giao tiếp | IPC (pipe, socket, shared memory) | Trực tiếp (bộ nhớ dùng chung) |
| Ảnh hưởng khi crash | Process khác không bị ảnh hưởng | Có thể làm crash toàn bộ process |
| Song song hóa | Song song thực sự trên nhiều CPU | Phụ thuộc ngôn ngữ/GIL |
| Cô lập | Mạnh | Yếu |
Bộ Nhớ: Điểm Khác Biệt Cốt Lõi
Đây là sự phân biệt quan trọng nhất. Hãy làm rõ bằng ví dụ cụ thể.
Process: Bộ nhớ cô lập
# process_memory.py
import multiprocessing
counter = 0 # Nằm trong bộ nhớ của process cha
def increment():
global counter
counter += 1
print(f"Counter trong process con: {counter}") # In ra 1
if __name__ == "__main__":
p = multiprocessing.Process(target=increment)
p.start()
p.join()
print(f"Counter trong process cha: {counter}") # Vẫn là 0!Counter trong process con: 1
Counter trong process cha: 0Process con nhận được bản sao bộ nhớ của cha (copy-on-write). Thay đổi trong con KHÔNG ảnh hưởng đến cha.
Thread: Bộ nhớ dùng chung
# thread_memory.py
import threading
counter = 0 # Dùng chung giữa tất cả thread
def increment():
global counter
counter += 1
print(f"Thread counter: {counter}")
threads = [threading.Thread(target=increment) for _ in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Counter cuối: {counter}") # 3 (tất cả thread sửa cùng biến)Thread counter: 1
Thread counter: 2
Thread counter: 3
Counter cuối: 3Tất cả thread đều sửa cùng một biến counter. Điều này mạnh mẽ nhưng nguy hiểm — nếu không đồng bộ hóa, bạn sẽ gặp race condition.
Giao Tiếp
Giao Tiếp Giữa Các Process (IPC)
Vì các process không thể chia sẻ bộ nhớ trực tiếp, chúng cần các cơ chế IPC rõ ràng:
Pipe — kênh dữ liệu một chiều:
import multiprocessing
def producer(conn):
conn.send("Xin chào từ process con!")
conn.close()
if __name__ == "__main__":
parent_conn, child_conn = multiprocessing.Pipe()
p = multiprocessing.Process(target=producer, args=(child_conn,))
p.start()
print(parent_conn.recv()) # "Xin chào từ process con!"
p.join()Shared memory — vùng được cấp phát rõ ràng mà cả hai process đều có thể truy cập:
import multiprocessing
def worker(shared_val):
shared_val.value += 10
if __name__ == "__main__":
val = multiprocessing.Value('i', 0) # Số nguyên dùng chung, bắt đầu từ 0
p = multiprocessing.Process(target=worker, args=(val,))
p.start()
p.join()
print(val.value) # 10Các cơ chế IPC khác bao gồm: socket, message queue, signal và memory-mapped file.
Giao Tiếp Giữa Các Thread
Thread giao tiếp đơn giản bằng cách đọc/ghi biến dùng chung — nhưng bạn phải dùng lock để tránh race condition:
import threading
counter = 0
lock = threading.Lock()
def safe_increment():
global counter
with lock: # Chỉ một thread vào tại một thời điểm
counter += 1
threads = [threading.Thread(target=safe_increment) for _ in range(1000)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # Luôn là 1000 (không có race condition)Đánh Đổi Hiệu Năng
Chi phí tạo
Tạo process tốn kém hơn nhiều so với tạo thread:
import time
import threading
import multiprocessing
def do_nothing():
pass
# Benchmark tạo thread
start = time.perf_counter()
threads = [threading.Thread(target=do_nothing) for _ in range(1000)]
for t in threads: t.start()
for t in threads: t.join()
thread_time = time.perf_counter() - start
# Benchmark tạo process
start = time.perf_counter()
processes = [multiprocessing.Process(target=do_nothing) for _ in range(100)]
for p in processes: p.start()
for p in processes: p.join()
process_time = time.perf_counter() - start
print(f"1000 thread: {thread_time:.3f}s")
print(f"100 process: {process_time:.3f}s")
# Thread rẻ hơn ~10-50 lần để tạoContext switching
Khi OS chuyển đổi thread/process nào chạy trên CPU:
- Chuyển thread: Lưu và khôi phục register + stack pointer. Bộ nhớ dùng chung vẫn trong cache.
- Chuyển process: Lưu và khôi phục toàn bộ ánh xạ bộ nhớ (page table). CPU cache bị xóa. Tốn kém hơn nhiều.
Song song hóa — GIL của Python
Python có Global Interpreter Lock (GIL), nghĩa là chỉ một thread chạy code Python tại một thời điểm — dù trên CPU nhiều nhân. Với tác vụ CPU-bound trong Python, thread không giúp ích:
# CPU-bound: thread sẽ không song song hóa được (GIL)
import threading, time
def count_to_million():
count = 0
while count < 1_000_000:
count += 1
# Đơn luồng
start = time.perf_counter()
count_to_million()
count_to_million()
print(f"Tuần tự: {time.perf_counter() - start:.2f}s")
# Đa luồng (GIL làm cho cái này KHÔNG nhanh hơn)
start = time.perf_counter()
t1 = threading.Thread(target=count_to_million)
t2 = threading.Thread(target=count_to_million)
t1.start(); t2.start()
t1.join(); t2.join()
print(f"2 thread: {time.perf_counter() - start:.2f}s") # Tương đương hoặc chậm hơn!Với code CPU-bound Python, hãy dùng multiprocessing (mỗi process có GIL riêng):
import multiprocessing, time
def count_to_million():
count = 0
while count < 1_000_000:
count += 1
start = time.perf_counter()
p1 = multiprocessing.Process(target=count_to_million)
p2 = multiprocessing.Process(target=count_to_million)
p1.start(); p2.start()
p1.join(); p2.join()
print(f"2 process: {time.perf_counter() - start:.2f}s") # Nhanh hơn ~2 lần trên multi-core!Go và Java không có hạn chế này — thread của chúng thực sự chạy song song trên nhiều CPU.
Go: Goroutine (Thread Siêu Nhẹ)
Go có cách tiếp cận khác với goroutine — "green thread" cực kỳ nhẹ được quản lý bởi Go runtime (không phải OS). Bạn có thể chạy hàng triệu goroutine với ít overhead.
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d đang chạy\n", id)
}
func main() {
var wg sync.WaitGroup
// Tạo 10 goroutine
for i := 1; i <= 10; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("Tất cả worker đã xong")
}Goroutine giao tiếp qua channel (thay vì shared memory):
package main
import "fmt"
func produce(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // Gửi vào channel
}
close(ch)
}
func main() {
ch := make(chan int)
go produce(ch)
for val := range ch { // Nhận từ channel
fmt.Println("Nhận được:", val)
}
}Phương châm của Go: "Đừng giao tiếp bằng cách chia sẻ bộ nhớ; hãy chia sẻ bộ nhớ bằng cách giao tiếp."
Java: Thread và Virtual Thread
Java dùng OS thread theo mặc định. Từ Java 21, virtual thread (Project Loom) cung cấp concurrency nhẹ tương tự goroutine của Go:
// OS thread truyền thống
Thread thread = new Thread(() -> {
System.out.println("Chạy trong OS thread: " + Thread.currentThread());
});
thread.start();
thread.join();
// Virtual thread (Java 21+) — rẻ hơn nhiều
Thread vThread = Thread.ofVirtual().start(() -> {
System.out.println("Chạy trong virtual thread: " + Thread.currentThread());
});
vThread.join();
// Tạo hàng nghìn virtual thread dễ dàng
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId);
});
}
}Khi Nào Dùng Process, Khi Nào Dùng Thread
Dùng process khi:
- Cô lập quan trọng — crash ở một worker không được ảnh hưởng đến worker khác (web server, microservice)
- Tác vụ CPU-bound trong Python — vượt qua GIL
- Ranh giới bảo mật — các process có thể có quyền khác nhau
- Tác vụ độc lập — không cần giao tiếp chặt chẽ
- Chạy chương trình khác — spawn lệnh shell, một binary riêng biệt
Ví dụ thực tế: Nginx worker process, Python multiprocessing, tab Chrome, Celery worker
Dùng thread khi:
- Tác vụ I/O-bound — chờ network, disk, database (thread chờ rất rẻ)
- Cần shared state — thread tự nhiên chia sẻ bộ nhớ
- Overhead thấp quan trọng — tạo thread rẻ
- Java/Go/C++/Rust — thread thực sự song song (không có GIL)
- UI phản hồi nhanh — giữ main thread rảnh trong khi background thread làm việc
Ví dụ thực tế: Handler yêu cầu web server, tác vụ nền trong GUI app, database connection pool
Sơ đồ quyết định
Các Lỗi Thường Gặp
1. Race condition (thread)
# SAI — race condition
import threading
balance = 1000
def withdraw(amount):
global balance
if balance >= amount:
# Thread khác có thể chạy ở đây, gây rút tiền 2 lần!
balance -= amount
threads = [threading.Thread(target=withdraw, args=(100,)) for _ in range(20)]
for t in threads: t.start()
for t in threads: t.join()
print(balance) # Có thể bị âm!Cách sửa: dùng lock hoặc threading.local() cho trạng thái thread-local.
2. Deadlock (thread)
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def thread1():
with lock_a:
with lock_b: # Thread 1 giữ A, chờ B
print("Thread 1 xong")
def thread2():
with lock_b:
with lock_a: # Thread 2 giữ B, chờ A — DEADLOCK
print("Thread 2 xong")Cách sửa: luôn lấy lock theo cùng một thứ tự.
3. Zombie process
Một process đã kết thúc nhưng process cha chưa gọi wait() trở thành zombie — nó vẫn còn entry trong process table.
import multiprocessing, time
def child():
print("Con xong")
p = multiprocessing.Process(target=child)
p.start()
# Luôn gọi p.join() hoặc p.wait() để dọn dẹp!
p.join()Tổng Kết
Process:
- Chương trình độc lập với bộ nhớ riêng
- Cô lập — crash không ảnh hưởng đến process khác
- Tốn kém để tạo, tốn kém để chuyển đổi
- Giao tiếp qua IPC (pipe, socket, shared memory)
- Tốt nhất cho: cô lập crash, CPU-bound Python, ranh giới bảo mật
Thread:
- Đơn vị thực thi bên trong process
- Chia sẻ bộ nhớ với các thread cùng process — giao tiếp nhanh, nhưng có nguy cơ race condition
- Rẻ để tạo, rẻ hơn để chuyển đổi
- Giao tiếp qua shared memory + lock
- Tốt nhất cho: tác vụ I/O-bound, shared state, CPU-bound với Java/Go/Rust
Quy tắc chung: nếu cần cô lập, dùng process. Nếu cần tốc độ và shared state, dùng thread.
Hiểu sự khác biệt này sẽ giúp bạn thiết kế hệ thống tốt hơn, debug lỗi concurrency nhanh hơn và đưa ra quyết định kiến trúc thông minh hơn.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.