Back to blog

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

operating-systemsconcurrencybackendpythongo
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áiMô tả
NewĐang được tạo
ReadyChờ được cấp CPU
RunningĐang thực thi trên CPU
Blocked/WaitingChờ 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ầnThread 1Thread 2Thread 3
Heap (dùng chung)✅✅✅
Code (dùng chung)✅✅✅
Biến toàn cục (dùng chung)✅✅✅
Stack (riêng)Stack 1Stack 2Stack 3
Program Counter (riêng)PC 1PC 2PC 3

So Sánh Các Điểm Khác Biệt Chính

Khía cạnhProcessThread
Bộ nhớCô lập — không gian địa chỉ riêngChia sẻ trong process
Chi phí tạoCao (OS phải thiết lập bộ nhớ)Thấp (dùng chung bộ nhớ cha)
Context switchTố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ếpIPC (pipe, socket, shared memory)Trực tiếp (bộ nhớ dùng chung)
Ảnh hưởng khi crashProcess khác không bị ảnh hưởngCó thể làm crash toàn bộ process
Song song hóaSong song thực sự trên nhiều CPUPhụ thuộc ngôn ngữ/GIL
Cô lậpMạnhYế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: 0

Process 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: 3

Tấ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)  # 10

Cá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ạo

Context 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.