GIL và Threading trong Python: hiểu đúng để dùng đúng
Nếu bạn từng nghe “Python không chạy đa luồng được vì có GIL” thì câu đó vừa đúng vừa sai. Thread trong Python có thể làm chương trình tải 100 trang web nhanh gấp 10 lần, nhưng cũng có thể làm một phép tính nặng chậm đi. Và dù có GIL, code đa luồng của bạn vẫn có thể bị race condition.
Trong bài này, bạn sẽ học:
- Phân biệt concurrency và parallelism, tác vụ I/O-bound và CPU-bound
- Tạo và quản lý thread với
threading.ThreadvàThreadPoolExecutor - GIL là gì, vì sao CPython cần nó, và nó được nhả ra khi nào
- Vì sao race condition vẫn xảy ra khi có GIL và cách sửa bằng
Lock - Dùng các công cụ đồng bộ:
RLock,Condition,Semaphore,Event,Barrier,Queue - Nhận diện và tránh deadlock
- Python free-threaded (không GIL) trong 3.13/3.14 thay đổi điều gì
Các số liệu đo trong bài chạy trên CPython 3.13 (macOS, Apple Silicon). Máy của bạn sẽ cho con số khác, nhưng tỉ lệ tương tự.
Concurrency, parallelism và hai loại tác vụ
Phần tiêu đề “Concurrency, parallelism và hai loại tác vụ”Trước khi viết dòng code nào, cần phân biệt hai khái niệm hay bị dùng lẫn:
- Concurrency (đồng thời): nhiều việc cùng đang diễn ra, xen kẽ nhau. Một đầu bếp vừa chờ nồi nước sôi vừa thái rau.
- Parallelism (song song): nhiều việc thực sự chạy cùng một lúc trên nhiều lõi CPU. Hai đầu bếp, mỗi người thái một rổ rau.
Và hai loại tác vụ:
| Loại | Thời gian chủ yếu dành cho | Ví dụ |
|---|---|---|
| I/O-bound | chờ: mạng, ổ đĩa, database | gọi API, tải file, truy vấn SQL |
| CPU-bound | tính toán | xử lý ảnh, mã hoá, tính toán số học thuần Python |
Như bạn sẽ thấy, thread trong CPython (có GIL) cho bạn concurrency - rất hợp với I/O-bound, nhưng không cho parallelism với code Python thuần - nên không giúp gì cho CPU-bound.
Thread đầu tiên
Phần tiêu đề “Thread đầu tiên”import threadingimport time
def download(name, seconds): print(f"[{threading.current_thread().name}] bắt đầu tải {name}") time.sleep(seconds) # giả lập chờ mạng print(f"[{threading.current_thread().name}] xong {name}")
t1 = threading.Thread(target=download, args=("a.zip", 1), name="T1")t2 = threading.Thread(target=download, args=("b.zip", 1), name="T2")
start = time.perf_counter()t1.start()t2.start()t1.join() # chờ t1 kết thúct2.join()print(f"Tổng: {time.perf_counter() - start:.2f}s") # ~1.00s chứ không phải 2sVài điều cần nắm:
start()tạo một thread hệ điều hành thật (không phải “green thread”), rồi chạytargettrong đó.join()chặn thread hiện tại cho tới khi thread kia xong. Quênjoin()là nguồn bug phổ biến: chương trình dùng kết quả khi thread chưa làm xong.daemon=Trueđánh dấu thread “phụ”: khi mọi thread không phải daemon kết thúc, chương trình thoát ngay lập tức và giết luôn daemon thread - kể cả khi nó đang ghi dở file. Chỉ dùng daemon cho việc có thể bỏ ngang an toàn (heartbeat, theo dõi).
Cách hiện đại: ThreadPoolExecutor
Phần tiêu đề “Cách hiện đại: ThreadPoolExecutor”Trong thực tế bạn hiếm khi tự tạo Thread. concurrent.futures.ThreadPoolExecutor quản lý một nhóm thread, tái sử dụng chúng, và trả về kết quả (hoặc exception) cho bạn:
import timefrom concurrent.futures import ThreadPoolExecutor
def fetch(url): time.sleep(0.5) # giả lập request mất 0.5s return f"nội dung {url}"
urls = [f"https://example.com/{i}" for i in range(10)]
start = time.perf_counter()for url in urls: fetch(url)print(f"Tuần tự: {time.perf_counter() - start:.2f}s") # 5.04s
start = time.perf_counter()with ThreadPoolExecutor(max_workers=10) as pool: results = list(pool.map(fetch, urls))print(f"10 thread: {time.perf_counter() - start:.2f}s") # 0.51sNhanh gấp 10 lần - vì trong lúc một thread “ngủ” chờ mạng, các thread khác được chạy.
GIL là gì?
Phần tiêu đề “GIL là gì?”GIL (Global Interpreter Lock) là một khoá duy nhất trong mỗi interpreter CPython. Quy tắc đơn giản: tại mỗi thời điểm, chỉ thread nào đang giữ GIL mới được chạy bytecode Python.
Vì sao CPython cần GIL?
Phần tiêu đề “Vì sao CPython cần GIL?”Nhớ lại bài Object trong bộ nhớ: mọi object đều có ob_refcnt, và nó thay đổi liên tục - mỗi lần gán biến, truyền tham số, thêm vào list. Nếu hai thread cùng tăng/giảm refcount của một object mà không có khoá, refcount sẽ sai → object bị giải phóng khi vẫn đang dùng (crash) hoặc không bao giờ được giải phóng (leak).
Có hai lựa chọn:
- Đặt khoá nhỏ cho từng object → mọi thao tác đều tốn thêm chi phí khoá/mở, code đơn luồng chậm đi đáng kể, và rất dễ deadlock.
- Đặt một khoá to cho cả interpreter → đơn giản, code đơn luồng nhanh, extension C dễ viết.
CPython chọn cách 2 từ những năm 1990. Nó là một phần lý do hệ sinh thái extension C (NumPy, lxml, …) phát triển mạnh.
Kiểm chứng: CPU-bound không nhanh hơn
Phần tiêu đề “Kiểm chứng: CPU-bound không nhanh hơn”import threadingimport time
def count(n): total = 0 for i in range(n): total += i return total
N = 20_000_000
start = time.perf_counter()count(N)count(N)print(f"Tuần tự: {time.perf_counter() - start:.2f}s") # 1.28s
start = time.perf_counter()t1 = threading.Thread(target=count, args=(N,))t2 = threading.Thread(target=count, args=(N,))t1.start(); t2.start()t1.join(); t2.join()print(f"2 thread: {time.perf_counter() - start:.2f}s") # 1.22s - gần như không đổiHai thread thay nhau giữ GIL nên tổng thời gian vẫn như chạy tuần tự. Trên các phiên bản cũ (trước 3.2) hoặc trên máy nhiều lõi, đôi khi còn chậm hơn do chi phí tranh giành GIL.
Khi nào GIL được nhả ra?
Phần tiêu đề “Khi nào GIL được nhả ra?”Đây là phần quan trọng nhất để hiểu vì sao thread vẫn hữu ích:
-
Khi chờ I/O:
socket.recv,file.read,time.sleep, truy vấn database… đều nhả GIL trước khi gọi hệ điều hành và lấy lại khi xong. -
Theo chu kỳ: thread đang chạy bị yêu cầu nhả GIL sau mỗi switch interval (mặc định 5ms) để thread khác có cơ hội chạy.
import sysprint(sys.getswitchinterval()) # 0.005 -
Trong extension C làm việc nặng: nhiều thư viện nhả GIL khi xử lý dữ liệu lớn mà không đụng tới object Python -
hashlib,zlib, NumPy (nhiều phép toán), đọc/ghi ảnh trong Pillow…
Điểm 3 có nghĩa là thread có thể cho parallelism thật với đúng loại công việc:
import hashlibimport timefrom concurrent.futures import ThreadPoolExecutor
data = b"x" * 50_000_000 # 50 MB
def digest(_): return hashlib.sha256(data).hexdigest()
start = time.perf_counter()for i in range(4): digest(i)print(f"Tuần tự: {time.perf_counter() - start:.2f}s") # 0.08s
start = time.perf_counter()with ThreadPoolExecutor(4) as pool: list(pool.map(digest, range(4)))print(f"4 thread: {time.perf_counter() - start:.2f}s") # 0.02s - nhanh gấp 4!hashlib nhả GIL khi băm dữ liệu lớn hơn 2 KB, nên 4 thread chạy thật sự song song trên 4 lõi.
Race condition: GIL không bảo vệ code của bạn
Phần tiêu đề “Race condition: GIL không bảo vệ code của bạn”Đây là hiểu lầm nguy hiểm nhất: “đã có GIL thì chỉ một thread chạy tại một thời điểm, nên không cần khoá”. Sai. GIL chỉ đảm bảo mỗi lệnh bytecode được thực thi trọn vẹn. Còn một dòng Python thường gồm nhiều lệnh bytecode (xem bài Bytecode và dis), và thread có thể bị chuyển giữa các lệnh đó.
Tái hiện lỗi
Phần tiêu đề “Tái hiện lỗi”Giả sử ta có một tài khoản ngân hàng và 4 thread cùng nạp tiền. Hàm audit tượng trưng cho bất kỳ lời gọi hàm nào (ghi log, kiểm tra…) nằm giữa bước đọc và bước ghi:
import threading
class Account: def __init__(self): self.balance = 0
def audit(amount): return amount # ví dụ: ghi log, kiểm tra hạn mức...
def deposit(account, times): for _ in range(times): current = account.balance # (1) đọc current = audit(current) # (2) làm gì đó account.balance = current + 1 # (3) ghi
account = Account()threads = [threading.Thread(target=deposit, args=(account, 200_000)) for _ in range(4)]for t in threads: t.start()for t in threads: t.join()
print(account.balance) # mong đợi 800000Chạy ba lần, mình nhận được:
400000478637583405Mất từ 27% tới 50% số tiền! Kịch bản xảy ra:
Thread A: đọc balance = 100 ── chuyển thread ──Thread B: đọc balance = 100Thread B: ghi balance = 101 ── chuyển thread ──Thread A: ghi balance = 101 <- lần nạp của B bị ghi đè, mất 1 đồngLỗi này đặc biệt nguy hiểm vì nó không ổn định: có thể chạy đúng 1000 lần trên máy bạn rồi sai trên server, phụ thuộc vào thời điểm chuyển thread. Thậm chí với vòng lặp đơn giản counter += 1 không có lời gọi hàm, CPython 3.10+ hiếm khi chuyển thread đúng chỗ đó nên lỗi có thể “trốn” hoàn toàn khi bạn test - nhưng không có gì đảm bảo, và trên bản Python free-threaded thì nó lộ ra ngay lập tức.
Sửa bằng Lock
Phần tiêu đề “Sửa bằng Lock”Critical section (vùng găng) là đoạn code đọc-sửa-ghi dữ liệu dùng chung. Ta bọc nó bằng Lock để chỉ một thread được vào tại một thời điểm:
import threading
class Account: def __init__(self): self.balance = 0 self._lock = threading.Lock()
def deposit(self, amount): with self._lock: # acquire() khi vào, release() khi ra (kể cả khi lỗi) current = self.balance self.balance = current + amount
def worker(account, times): for _ in range(times): account.deposit(1)
account = Account()threads = [threading.Thread(target=worker, args=(account, 200_000)) for _ in range(4)]for t in threads: t.start()for t in threads: t.join()print(account.balance) # 800000 - luôn đúngLưu ý thiết kế: khoá nằm trong class cùng với dữ liệu nó bảo vệ. Người dùng class không cần biết tới khoá - đây là cách tổ chức giúp tránh quên khoá.
Cái giá: bản có khoá chạy mất 0.12s so với 0.05s. Giữ critical section càng ngắn càng tốt - đừng gọi mạng hay ghi file khi đang giữ khoá.
Thao tác nào “an toàn” sẵn?
Phần tiêu đề “Thao tác nào “an toàn” sẵn?”Trong CPython, các thao tác đơn lẻ trên kiểu built-in được thực hiện trong một lệnh C nên là nguyên tử: list.append(x), list.pop(), dict[k] = v, dict.get(k), deque.append/popleft…
Nhưng tổ hợp của chúng thì không:
# KHÔNG an toàn: kiểm tra rồi hành động (check-then-act)if key not in cache: cache[key] = compute(key) # hai thread có thể cùng compute
# KHÔNG an toàn: đọc-sửa-ghicounts[word] = counts.get(word, 0) + 1Đừng dựa vào tính nguyên tử “ngầm” này để thiết kế - nó là chi tiết cài đặt. Dùng khoá hoặc Queue.
Bộ công cụ đồng bộ hoá
Phần tiêu đề “Bộ công cụ đồng bộ hoá”RLock - khoá có thể vào lại
Phần tiêu đề “RLock - khoá có thể vào lại”Một Lock thường sẽ tự deadlock nếu cùng một thread acquire hai lần:
import threading
lock = threading.Lock()with lock: # with lock: # <- treo mãi mãi: thread tự chờ chính mình passĐiều này hay xảy ra khi một phương thức có khoá gọi một phương thức khác cũng có khoá. RLock (reentrant lock) cho phép thread đang giữ khoá acquire tiếp, và chỉ thực sự mở khi số lần release bằng số lần acquire:
import threading
class Inventory: def __init__(self): self._lock = threading.RLock() self._items = {}
def add(self, name, qty): with self._lock: self._items[name] = self._items.get(name, 0) + qty
def add_many(self, pairs): with self._lock: # giữ khoá cho CẢ lô for name, qty in pairs: self.add(name, qty) # add() acquire lại - OK với RLockEvent - bật/tắt tín hiệu giữa các thread
Phần tiêu đề “Event - bật/tắt tín hiệu giữa các thread”Event là một cờ boolean an toàn đa luồng. Ứng dụng kinh điển: dừng worker một cách lịch sự.
import threadingimport time
stop = threading.Event()
def heartbeat(): while not stop.is_set(): print("tick") stop.wait(timeout=1.0) # ngủ tối đa 1s, nhưng thức dậy NGAY khi stop được set print("worker dừng sạch sẽ")
t = threading.Thread(target=heartbeat)t.start()time.sleep(2.5)stop.set()t.join()Dùng stop.wait(timeout) thay cho time.sleep() giúp thread phản ứng tức thì khi được yêu cầu dừng.
Semaphore - giới hạn số thread cùng làm một việc
Phần tiêu đề “Semaphore - giới hạn số thread cùng làm một việc”Semaphore(n) cho phép tối đa n thread vào vùng được bảo vệ. Rất hợp để không làm quá tải server bên ngoài:
import threadingimport timefrom concurrent.futures import ThreadPoolExecutor
api_slots = threading.BoundedSemaphore(3) # API chỉ cho 3 kết nối đồng thời
def call_api(i): with api_slots: print(f"gọi API {i}") time.sleep(0.5) return i
with ThreadPoolExecutor(max_workers=10) as pool: list(pool.map(call_api, range(9))) # chạy thành 3 đợt, mỗi đợt 3 requestBoundedSemaphore báo lỗi nếu bạn release() nhiều hơn acquire() - luôn ưu tiên nó hơn Semaphore thường.
Condition - chờ một điều kiện trở thành đúng
Phần tiêu đề “Condition - chờ một điều kiện trở thành đúng”Condition kết hợp một khoá với khả năng “ngủ cho tới khi được đánh thức”. Ví dụ: một bộ đệm có giới hạn giữa producer và consumer:
import threadingfrom collections import deque
class BoundedBuffer: def __init__(self, capacity): self._items = deque() self._capacity = capacity self._cond = threading.Condition()
def put(self, item): with self._cond: # wait_for tự lặp lại việc kiểm tra -> an toàn trước "spurious wakeup" self._cond.wait_for(lambda: len(self._items) < self._capacity) self._items.append(item) self._cond.notify_all()
def get(self): with self._cond: self._cond.wait_for(lambda: len(self._items) > 0) item = self._items.popleft() self._cond.notify_all() return itemViết Condition đúng khá khó. Trong 95% trường hợp, bạn nên dùng thứ đã được viết sẵn: queue.Queue.
queue.Queue - cách giao tiếp an toàn nhất
Phần tiêu đề “queue.Queue - cách giao tiếp an toàn nhất”Thay vì nhiều thread cùng sửa một cấu trúc dữ liệu, hãy để chúng gửi thông điệp cho nhau qua hàng đợi. queue.Queue đã có sẵn mọi khoá cần thiết:
import queueimport threadingimport time
tasks = queue.Queue(maxsize=100) # put() sẽ chờ nếu đầy -> tự điều tiết producerSENTINEL = None
def worker(worker_id): while True: item = tasks.get() try: if item is SENTINEL: return time.sleep(0.1) # xử lý print(f"worker {worker_id} xử lý {item}") finally: tasks.task_done()
workers = [threading.Thread(target=worker, args=(i,)) for i in range(3)]for w in workers: w.start()
for job in range(10): tasks.put(job)
tasks.join() # chờ tới khi mọi job được task_done()for _ in workers: tasks.put(SENTINEL) # mỗi worker một "viên thuốc độc" để dừngfor w in workers: w.join()Mẫu “producer - hàng đợi - nhiều consumer - sentinel” này là xương sống của rất nhiều hệ thống thực tế: crawler, xử lý log, pipeline dữ liệu.
Barrier - chờ đủ người rồi mới đi
Phần tiêu đề “Barrier - chờ đủ người rồi mới đi”import threading
barrier = threading.Barrier(3)
def player(name): print(f"{name} đã sẵn sàng") barrier.wait() # chờ đủ 3 thread tới đây print(f"{name} xuất phát!")
for n in ["An", "Bình", "Chi"]: threading.Thread(target=player, args=(n,)).start()Deadlock và cách phòng tránh
Phần tiêu đề “Deadlock và cách phòng tránh”Deadlock xảy ra khi các thread chờ nhau theo vòng tròn:
Thread 1: giữ lock_a, chờ lock_bThread 2: giữ lock_b, chờ lock_a -> cả hai chờ nhau mãi mãiimport threading
lock_a = threading.Lock()lock_b = threading.Lock()
def transfer_1(): with lock_a: with lock_b: # thứ tự A -> B pass
def transfer_2(): with lock_b: with lock_a: # thứ tự B -> A <- nguy cơ deadlock passCác nguyên tắc phòng tránh:
-
Luôn acquire nhiều khoá theo cùng một thứ tự trên toàn chương trình. Ví dụ khi chuyển tiền giữa hai tài khoản, khoá tài khoản có
idnhỏ hơn trước:def transfer(src, dst, amount):first, second = sorted([src, dst], key=lambda acc: acc.id)with first.lock, second.lock:src.balance -= amountdst.balance += amount -
Dùng timeout để phát hiện thay vì treo vĩnh viễn:
if lock.acquire(timeout=5):try:...finally:lock.release()else:raise TimeoutError("có thể đang deadlock") -
Không gọi code “lạ” (callback của người dùng, hàm có thể acquire khoá khác) trong khi đang giữ khoá.
-
Giảm chia sẻ: dữ liệu càng ít dùng chung, càng ít khoá, càng ít deadlock.
Queuegiúp điều này.
Khi chương trình bị treo, faulthandler in stack của mọi thread để bạn thấy ai đang chờ ai:
import faulthandler, sysfaulthandler.dump_traceback_later(30, exit=False, file=sys.stderr) # in stack nếu sau 30s chưa xongthreading.local - dữ liệu riêng cho từng thread
Phần tiêu đề “threading.local - dữ liệu riêng cho từng thread”Đôi khi mỗi thread cần bản riêng của một thứ không an toàn khi chia sẻ, như kết nối database:
import sqlite3import threading
_local = threading.local()
def get_conn(): # mỗi thread lần đầu gọi sẽ tạo connection riêng, lần sau dùng lại if not hasattr(_local, "conn"): _local.conn = sqlite3.connect("app.db") return _local.connVới code asyncio, dùng contextvars thay cho threading.local (xem bài asyncio chuyên sâu).
ThreadPoolExecutor chuyên sâu
Phần tiêu đề “ThreadPoolExecutor chuyên sâu”submit và as_completed: xử lý kết quả ngay khi có
Phần tiêu đề “submit và as_completed: xử lý kết quả ngay khi có”map trả kết quả theo thứ tự đầu vào. Nếu muốn xử lý việc nào xong trước thì làm trước, dùng submit + as_completed:
import randomimport timefrom concurrent.futures import ThreadPoolExecutor, as_completed
def fetch(url): time.sleep(random.uniform(0.1, 1.0)) if url.endswith("3"): raise ConnectionError(f"lỗi khi tải {url}") return len(url)
urls = [f"https://site/{i}" for i in range(6)]
with ThreadPoolExecutor(max_workers=4) as pool: future_to_url = {pool.submit(fetch, url): url for url in urls} for future in as_completed(future_to_url): url = future_to_url[future] try: print(url, "->", future.result()) except ConnectionError as exc: print(url, "thất bại:", exc)Điều quan trọng: exception trong thread không tự hiện ra. Nó được cất trong Future và chỉ được ném lại khi bạn gọi future.result(). Nếu bạn submit mà không bao giờ đọc kết quả, lỗi sẽ biến mất trong im lặng.
Chọn max_workers
Phần tiêu đề “Chọn max_workers”- Mặc định:
min(32, os.cpu_count() + 4). - Với I/O-bound, có thể tăng lên vài chục - nhưng hãy tôn trọng giới hạn của hệ thống phía bên kia (database pool, rate limit của API).
- Mỗi thread tốn bộ nhớ stack (thường vài trăm KB tới vài MB ảo) và chi phí chuyển ngữ cảnh. Hàng nghìn kết nối đồng thời là lúc nên cân nhắc
asyncio.
Timeout và huỷ
Phần tiêu đề “Timeout và huỷ”from concurrent.futures import ThreadPoolExecutor, TimeoutError
with ThreadPoolExecutor() as pool: future = pool.submit(fetch, "https://site/1") try: result = future.result(timeout=2) except TimeoutError: future.cancel() # chỉ huỷ được nếu CHƯA bắt đầu chạyPython không có cách an toàn để giết một thread đang chạy. Muốn dừng giữa chừng, thread phải tự kiểm tra một Event như ví dụ ở trên.
Python free-threaded: tương lai không GIL
Phần tiêu đề “Python free-threaded: tương lai không GIL”PEP 703 đưa vào CPython một bản build không có GIL:
- Python 3.13: thử nghiệm, cài riêng dưới tên
python3.13t. - Python 3.14: được hỗ trợ chính thức (PEP 779) nhưng vẫn là bản build tuỳ chọn
python3.14t; bản mặc định vẫn có GIL.
Chạy lại chính ví dụ đếm số ở trên với python3.14t:
CPython 3.14 (GIL) CPython 3.14t (free-threaded)Tuần tự 1.06s 0.98s2 thread 1.05s 0.50s <- song song thật!Nhưng hãy xem ví dụ race condition:
Mong đợi 4000000 GIL: 4000000 free-threaded: 1723353Không còn GIL, những lỗi đồng bộ “ngủ yên” trong code cũ sẽ lộ ra. Bạn có thể kiểm tra đang chạy bản nào:
import sysprint(sys._is_gil_enabled()) # False trên bản free-threaded (3.13+)Bản free-threaded đổi lại vài thứ: code đơn luồng chậm hơn một chút, một số extension C chưa tương thích (khi import chúng, GIL có thể tự bật lại). Nhưng xu hướng đã rõ: hãy viết code đa luồng đúng đắn ngay từ bây giờ - dùng khoá và Queue cho dữ liệu dùng chung, đừng dựa vào GIL.
Những lỗi thường gặp
Phần tiêu đề “Những lỗi thường gặp”| Lỗi | Triệu chứng | Cách sửa |
|---|---|---|
| Dùng thread cho tính toán Python thuần | Không nhanh hơn | ProcessPoolExecutor |
| Nghĩ GIL chống race condition | Số liệu sai ngẫu nhiên | Lock / Queue |
Quên join() hoặc result() |
Dùng kết quả chưa có; lỗi bị nuốt | Dùng with ThreadPoolExecutor() và đọc result() |
| Giữ khoá khi làm I/O | Chương trình chậm như chạy tuần tự | Thu nhỏ critical section |
| Acquire nhiều khoá khác thứ tự | Treo ngẫu nhiên | Quy ước thứ tự, dùng timeout |
| Daemon thread ghi file | File hỏng khi thoát | Thread thường + Event để dừng |
Bài tập
Phần tiêu đề “Bài tập”- Viết hàm
download_all(urls, max_concurrency)dùngThreadPoolExecutorvàBoundedSemaphore, trả về dict{url: số byte}và danh sách URL lỗi. - Viết class
ThreadSafeCountercóincrement(),value. Kiểm tra bằng 8 thread, mỗi thread tăng 100.000 lần. - Viết pipeline 3 giai đoạn: thread đọc dòng từ file →
Queue→ 4 thread chuyển thành chữ hoa →Queue→ thread ghi ra file. Dừng pipeline bằng sentinel.
Kết luận
Phần tiêu đề “Kết luận”Bạn đã đi từ khái niệm tới cơ chế bên trong của threading trong Python:
- Thread trong CPython là thread hệ điều hành thật, nhưng GIL cho phép chỉ một thread chạy bytecode tại một thời điểm.
- GIL được nhả khi chờ I/O và trong nhiều thư viện C → thread rất hiệu quả cho I/O-bound và cho các thư viện như
hashlib, NumPy. - GIL không làm code của bạn an toàn: mọi thao tác đọc-sửa-ghi trên dữ liệu dùng chung cần
Lock. - Ưu tiên
ThreadPoolExecutorvàqueue.Queuethay vì tự quản lý thread và khoá. - Python free-threaded đang tới - code đồng bộ hoá đúng hôm nay sẽ chạy nhanh hơn ngày mai.
Bài tiếp theo: Multiprocessing - khi bạn cần dùng hết mọi lõi CPU cho tính toán.