[Python] Concurrency trong Python

Table of contents
- Lập trình Đồng thời (Concurrency) trong Máy tính và Python
- Threading trong Python
- Cơ chế quản lý Thread của Python và OS
- The Global Interpreter Lock (GIL): Thách thức Đặc thù của Python
- Multiprocessing trong Python
- Lập trình Bất đồng bộ với asyncio
- So sánh các Mô hình Đồng thời trong Python
- Tổng hợp Kiến thức
Các khái niệm quan trọng liên quan đến lập trình đồng thời trong Python là những kiến thức nền tảng giúp bạn viết các chương trình hiệu quả hơn, đặc biệt khi xử lý nhiều tác vụ cùng lúc.
Lập trình Đồng thời (Concurrency) trong Máy tính và Python
Concurrency là gì? Tại sao nó quan trọng?
Khái niệm: Lập trình đồng thời (Concurrency) là khả năng của một hệ thống xử lý nhiều tác vụ có vẻ như đang diễn ra cùng một lúc. Các tác vụ này có thể bắt đầu, chạy và hoàn thành trong những khoảng thời gian chồng chéo lên nhau. Nói một cách đơn giản, concurrency là việc quản lý nhiều việc cùng một lúc.
Ví dụ: Hãy tưởng tượng một đầu bếp trong bếp. Họ có thể đang thái rau, khuấy nồi súp trên bếp, và thỉnh thoảng kiểm tra lò nướng. Mặc dù người đầu bếp chỉ có một (tương tự như số lõi CPU), họ vẫn đang quản lý nhiều công việc đồng thời bằng cách chuyển đổi giữa chúng một cách hợp lý.
Tầm quan trọng: Concurrency rất quan trọng trong các ứng dụng hiện đại vì nhiều lý do:
Tính Phản hồi (Responsiveness): Giữ cho giao diện người dùng (UI) không bị "đơ" khi các tác vụ nền đang chạy. Ví dụ, một ứng dụng máy tính để bàn vẫn cho phép bạn nhấp chuột và tương tác trong khi nó đang xử lý một lượng lớn dữ liệu ở chế độ nền.
Hiệu quả (Efficiency): Tận dụng tốt hơn tài nguyên hệ thống, đặc biệt là trong thời gian chờ đợi các hoạt động Nhập/Xuất (Input/Output - I/O). Ví dụ, tải nhiều tệp từ internet cùng lúc sẽ nhanh hơn nhiều so với việc tải từng tệp một cách tuần tự, vì chương trình có thể bắt đầu tải tệp khác trong khi chờ phản hồi mạng cho tệp hiện tại.
Hiệu năng (Performance): Mặc dù liên quan chặt chẽ đến "parallelism" (sẽ nói rõ hơn ở phần sau), concurrency là bước đầu tiên để có thể tận dụng các bộ xử lý đa lõi cho các tác vụ tính toán nặng.
Trong Python: Ngôn ngữ Python cung cấp sẵn một số cơ chế tích hợp để đạt được concurrency, bao gồm các module
threading
,multiprocessing
, vàasyncio
. Mỗi cơ chế này có ưu và nhược điểm riêng, phù hợp với các loại bài toán khác nhau.
Concurrency vs. Parallelism
Concurrency (Đồng thời): Là việc xử lý nhiều tác vụ trong một khoảng thời gian. Nó tập trung vào việc cấu trúc chương trình để quản lý đồng thời nhiều luồng công việc. Concurrency có thể tồn tại ngay cả trên một CPU đơn lõi (single-core), thông qua việc chuyển đổi nhanh chóng giữa các tác vụ (time-slicing).
Parallelism (Song song): Là việc thực thi (executing) nhiều tác vụ chính xác tại cùng một thời điểm. Điều này đòi hỏi phải có nhiều đơn vị xử lý (ví dụ: nhiều lõi CPU). Parallelism tập trung vào việc tăng tốc độ thực thi bằng cách làm nhiều việc cùng lúc.
Mối quan hệ: Parallelism là một cách để hiện thực hóa concurrency. Một hệ thống có thể đồng thời (concurrent) mà không cần song song (parallel). Ví dụ, người đầu bếp (một lõi CPU) quản lý nhiều món ăn (concurrency) bằng cách chuyển đổi sự chú ý, nhưng nếu có thêm một đầu bếp phụ (lõi CPU thứ hai), họ có thể thực sự nấu hai món cùng lúc (parallelism).
Sự liên quan đến Python: Việc phân biệt rõ ràng hai khái niệm này là cực kỳ quan trọng trong Python, đặc biệt là khi làm việc với CPython (trình thông dịch Python phổ biến nhất). Lý do chính là sự tồn tại của Global Interpreter Lock (GIL) (sẽ được thảo luận chi tiết sau), thường ngăn cản việc thực thi song song thực sự của nhiều luồng (threads) trên nhiều lõi CPU cho các tác vụ tính toán nặng.
Sự hiểu lầm phổ biến giữa concurrency và parallelism là một rào cản lớn cho người mới bắt đầu. Việc làm rõ điều này ngay từ đầu giúp hiểu tại sao Python lại có các module
threading
,multiprocessing
vàasyncio
, và khi nào nên sử dụng từng cái.Ví dụ,
threading
cung cấp concurrency nhưng thường không cung cấp parallelism cho mã Python thuần túy tốn CPU trong CPython, trong khimultiprocessing
lại có thể.
Threading trong Python
Định nghĩa Thread và Module threading
Thread là gì?
Một thread (luồng) là đơn vị thực thi nhỏ nhất của một chương trình mà hệ điều hành (Operating System - OS) có thể quản lý một cách độc lập.
Nhiều thread có thể cùng tồn tại bên trong một process (tiến trình) duy nhất và chúng chia sẻ cùng một không gian bộ nhớ (bao gồm mã lệnh, dữ liệu toàn cục, heap). Điều này có nghĩa là chúng có thể truy cập và thay đổi cùng một dữ liệu.
Module threading
của Python
Python cung cấp module
threading
trong thư viện chuẩn để tạo và quản lý các thread.Module này cung cấp một giao diện lập trình ứng dụng (API) hướng đối tượng, bậc cao, hoạt động dựa trên các thread gốc của OS bên dưới. Nó giúp che giấu sự phức tạp của việc tương tác trực tiếp với API thread của OS.
Tạo và Quản lý Thread trong Python
Tạo Thread Cơ bản: Việc tạo thread khá đơn giản. Bạn tạo một đối tượng
threading.Thread
, chỉ định hàm mục tiêu (target
) mà thread sẽ thực thi, và các đối số (args
hoặckwargs
) cho hàm đó. Sau đó, gọi phương thức.start()
để bắt đầu thực thi thread và.join()
để đợi thread đó hoàn thành trước khi chương trình chính tiếp tục hoặc kết thúc.import threading import time def worker_function(name): print(f"Thread {name}: bắt đầu") time.sleep(2) # Giả lập công việc tốn thời gian print(f"Thread {name}: kết thúc") # Tạo thread thread1 = threading.Thread(target=worker_function, args=(1,)) thread2 = threading.Thread(target=worker_function, args=(2,)) # Bắt đầu thread thread1.start() thread2.start() # Đợi thread hoàn thành thread1.join() thread2.join() print("Tất cả các thread đã hoàn thành.")
Chia sẻ Dữ liệu (và Rủi ro): Các thread trong cùng một process chia sẻ bộ nhớ giúp việc chia sẻ dữ liệu giữa các thread trở nên dễ dàng (ví dụ: thông qua biến toàn cục hoặc các đối tượng được truyền vào). Tuy nhiên, đây cũng là nguồn gốc của các vấn đề phức tạp như Race Conditions (Tình trạng tranh chấp) - xảy ra khi nhiều thread cố gắng truy cập và sửa đổi cùng một dữ liệu chia sẻ đồng thời, dẫn đến kết quả không thể đoán trước và thường là sai lệch. Để ngăn chặn điều này, module
threading
cung cấp các công cụ đồng bộ hóa nhưLock
,RLock
,Semaphore
,Event
,Condition
. Việc sử dụng đúng các công cụ này là rất quan trọng để đảm bảo tính đúng đắn của chương trình đa luồng, nhưng chúng ta sẽ không đi sâu vào chi tiết ở đây để tránh làm bạn quá tải.Daemon Threads: Một khái niệm khác là daemon thread. Mặc định, chương trình Python sẽ không kết thúc cho đến khi tất cả các thread không phải daemon đã hoàn thành. Nếu bạn đánh dấu một thread là daemon (
thread.daemon = True
trước khistart()
), chương trình chính có thể thoát ngay cả khi thread daemon đó vẫn đang chạy. Daemon thread thường được dùng cho các tác vụ nền không quan trọng (ví dụ: ghi log, giám sát).
Khi nào nên dùng Threading
Tác vụ I/O-bound: Đây là loại tác vụ mà thời gian thực thi bị giới hạn chủ yếu bởi tốc độ của các hoạt động Nhập/Xuất (I/O) chứ không phải tốc độ của CPU. Ví dụ bao gồm:
Đọc hoặc ghi dữ liệu từ/đến ổ đĩa.
Gửi hoặc nhận dữ liệu qua mạng (ví dụ: gọi API web, truy vấn cơ sở dữ liệu từ xa).
Chờ đợi người dùng nhập liệu. Trong quá trình thực hiện các tác vụ I/O này, thread thường rơi vào trạng thái chờ (blocking wait). Ví dụ, khi yêu cầu dữ liệu từ một trang web, thread sẽ phải chờ đợi phản hồi từ máy chủ web qua mạng.
Lợi ích của Threading cho I/O-bound: Đây chính là nơi
threading
tỏa sáng trong CPython. Khi một thread thực hiện một thao tác I/O và bị chặn (block), OS (hoặc trình thông dịch Python trong một số trường hợp liên quan đến GIL) có thể tạm dừng thread đó và chuyển sang chạy một thread khác. Quan trọng hơn, đối với nhiều loại I/O blocking trong CPython, Global Interpreter Lock (GIL) thường được giải phóng. Điều này cho phép một thread khác trong cùng process có thể chiếm lấy GIL và thực thi mã Python. Kết quả là, chương trình có thể thực hiện các công việc khác (ví dụ: bắt đầu một yêu cầu mạng khác, xử lý dữ liệu đã nhận) trong khi thread đầu tiên đang chờ I/O. Điều này tạo ra sự chồng chéo (overlap) thời gian chờ đợi, giúp cải thiện đáng kể thông lượng (throughput) và tính phản hồi của ứng dụng.
threading
trong CPython không mang lại lợi ích về tốc độ trên máy tính đa lõi. Lý do chính là GIL (sẽ giải thích kỹ ở phần sau) chỉ cho phép một thread thực thi mã bytecode Python tại một thời điểm trong một process. Do đó, dù bạn có nhiều lõi CPU và tạo ra nhiều thread để chạy tác vụ CPU-bound, chúng vẫn phải thay phiên nhau giữ GIL để chạy, không thể thực thi song song thực sự trên các lõi khác nhau. Thậm chí, việc sử dụng nhiều thread cho tác vụ CPU-bound còn có thể làm chương trình chậm đi do chi phí phát sinh từ việc chuyển đổi ngữ cảnh giữa các thread và sự tranh chấp GIL.Cơ chế quản lý Thread của Python và OS
Ánh xạ tới Thread của OS:
Khi bạn tạo một
threading.Thread
trong Python (ít nhất là trong CPython trên các OS phổ biến như Linux, macOS, Windows), nó thường được ánh xạ trực tiếp tới một thread thực sự do OS quản lý (ví dụ: POSIX threads (pthreads) trên Linux/macOS, Windows threads).Python không tự mình "phát minh" lại cơ chế thread mà tận dụng khả năng của OS
Đa nhiệm Phân chia Thời gian (Preemptive Multitasking):
OS sử dụng một bộ lập lịch (scheduler) để quản lý việc thực thi các thread này.
Cơ chế phổ biến là đa nhiệm phân chia thời gian (đa nhiệm ưu tiên). Bộ lập lịch của HĐH sẽ cấp cho mỗi thread một khoảng thời gian nhỏ (time slice) để chạy trên CPU. Khi hết thời gian, hoặc khi một sự kiện ưu tiên cao hơn xảy ra (ví dụ: dữ liệu I/O đã sẵn sàng), HĐH sẽ chủ động (preemptively) tạm dừng thread đang chạy (lưu lại trạng thái của nó), và chọn một thread khác để chạy. Quá trình này diễn ra rất nhanh chóng (dưới 1ms), tạo ra ảo giác rằng nhiều thread đang chạy đồng thời, ngay cả trên một CPU đơn lõi.
Chuyển đổi Ngữ cảnh (Context Switching):
Đây là quá trình tạm dừng một thread và bắt đầu chạy một thread khác.
Nó bao gồm 2 việc:
Lưu trạng thái hiện tại của thread bị tạm dừng (giá trị các thanh ghi CPU, con trỏ lệnh,...)
Nạp trạng thái của thread sắp chạy.
Việc này không miễn phí; nó tiêu tốn một lượng thời gian CPU nhất định. Đây là một phần lý do tại sao việc có quá nhiều thread có thể gây tốn kém về hiệu năng.
Hiểu rằng thread Python là thread của HĐH giúp làm rõ tại sao chúng có thể chạy đồng thời (do HĐH quản lý) và cũng gợi ý về chi phí liên quan (chuyển đổi ngữ cảnh). Nó cũng tạo tiền đề để hiểu sự tương phản với cơ chế đa nhiệm hợp tác của asyncio
sau này, và quan trọng hơn, nó đặt bối cảnh cho việc giới thiệu về giới hạn do GIL áp đặt lên các thread HĐH này khi chúng chạy mã Python.
The Global Interpreter Lock (GIL): Thách thức Đặc thù của Python
GIL là gì và Tại sao nó tồn tại trong CPython?
Định nghĩa:
Global Interpreter Lock (GIL - Khóa Trình Thông dịch Toàn cục) là một mutex (khóa loại trừ lẫn nhau) được sử dụng bởi trình thông dịch CPython.
Mục đích chính của nó là bảo vệ quyền truy cập vào các đối tượng Python, ngăn chặn việc nhiều thread thực thi mã bytecode Python cùng một lúc bên trong một process duy nhất. Nói cách khác, tại bất kỳ thời điểm nào trong một process CPython, chỉ có một thread duy nhất có thể giữ GIL và thực thi mã bytecode Python.
Mục đích tồn tại: GIL được tạo ra để giải quyết một số vấn đề trong thiết kế ban đầu của CPython:
Đơn giản hóa Quản lý Bộ nhớ:
Python sử dụng cơ chế đếm tham chiếu (reference counting) để quản lý bộ nhớ một cách tự động (garbage collection). Mỗi đối tượng Python có một bộ đếm cho biết có bao nhiêu biến đang tham chiếu đến nó. Khi bộ đếm về 0, đối tượng được giải phóng.
Lúc này, GIL sẽ giúp việc quản lý bộ nhớ một cách an toàn hơn bằng cách ngăn chặn tình trạng tranh chấp (race condition) khi nhiều thread cùng lúc cố gắng tăng/giảm bộ đếm tham chiếu của cùng một đối tượng.
Ví dụ:
Luồng 1 đọc đếm tham chiếu là 2, định giảm còn 1.
Luồng 2 cùng lúc cũng đọc 2, giảm còn 1
Kết quả: Đếm sai (vẫn là 1 thay vì 0) khiến bộ nhớ không được giải phóng đúng, gây memory leak hoặc crash
Đơn giản hóa thiết kế CPython:
Khi Python được tạo ra, nó không được thiết kế để ưu tiên hiệu năng đa luồng. Mục tiêu lúc bấy giờ là làm Python dễ phát triển, dễ bảo trì, và chạy tốt trên các hệ thống đơn luồng phổ biến (máy tính cá nhân thập niên 90 thường chỉ có 1 CPU).
Nếu không có GIL, để hỗ trợ đa luồng an toàn, CPython có thể phải dùng nhiều khóa nhỏ (fine-grained locks) cho từng đối tượng hoặc cấu trúc dữ liệu. Điều này làm mã nguồn phức tạp hơn, khó bảo trì, và có thể gây chậm do overhead của việc khóa/mở khóa liên tục.
Tích hợp Dễ dàng các Thư viện C: Trong thời kỳ đầu của Python, nhiều thư viện C không được thiết kế để an toàn cho luồng (thread-safe). GIL cho phép tích hợp các thư viện này vào Python một cách dễ dàng hơn, vì nó đảm bảo rằng chỉ có một thread chạy mã Python (và có khả năng gọi vào mã C) tại một thời điểm, tránh được nhiều vấn đề tiềm ẩn về đồng thời trong các thư viện C đó.
Ảnh hưởng của GIL đến Hiệu năng Đa luồng
Khi CPU đa nhân trở nên phổ biến (từ thập niên 2000), nhược điểm của GIL lộ rõ đặc biệt trong các tác vụ CPU-bound khi nó ngăn CPython tận dụng đa luồng:
Nút cổ chai: Đây là hệ quả trực tiếp và quan trọng nhất của GIL. Vì chỉ một thread có thể giữ GIL và thực thi bytecode Python tại một thời điểm, các chương trình CPython đa luồng không thể đạt được tính song song (parallelism) thực sự cho các tác vụ CPU-bound trên các bộ xử lý đa lõi. Ngay cả khi máy tính của bạn có 4, 8 hay nhiều lõi hơn, và bạn tạo ra tương ứng số thread để chạy một tác vụ tính toán nặng, chỉ một lõi CPU có thể thực thi mã Python của process đó tại một thời điểm. Các thread khác phải chờ đợi để giành được GIL.
Suy giảm Hiệu năng: Trong trường hợp các tác vụ CPU-bound nặng, việc sử dụng nhiều thread thậm chí có thể làm giảm hiệu năng so với việc chạy trên một thread duy nhất. Nguyên nhân là do chi phí bổ sung của việc chuyển đổi ngữ cảnh giữa các thread và chi phí của việc các thread liên tục "tranh giành" nhau để có được GIL (GIL contention).
Ngoại lệ cho I/O-bound: Như đã đề cập ở phần Threading, GIL thường được giải phóng bởi thread đang thực thi khi nó thực hiện một lời gọi hệ thống chặn (blocking I/O call), ví dụ như đọc/ghi file, chờ dữ liệu mạng. Khi GIL được giải phóng, một thread khác đang chờ có thể nắm bắt GIL và bắt đầu thực thi mã Python. Đây là lý do tại sao threading
vẫn hiệu quả cho các tác vụ I/O-bound – nó cho phép sự chồng chéo của các khoảng thời gian chờ I/O.
Cơ chế Hoạt động Bên trong: Cấp phát và Giải phóng Khóa
Thực thi Bytecode: Trình thông dịch CPython không thực thi trực tiếp mã nguồn Python (.py). Nó đầu tiên biên dịch mã nguồn thành một dạng mã trung gian gọi là bytecode. Sau đó, trình thông dịch sẽ thực thi các chỉ thị bytecode này từng cái một.
Kiểm tra Định kỳ (Check Interval): Để ngăn một thread CPU-bound giữ GIL mãi mãi và làm các thread khác "đói", CPython thực hiện một cơ chế giải phóng và cấp phát lại GIL một cách định kỳ.
Lịch sử, cơ chế này dựa trên số lượng chỉ thị bytecode đã thực thi (gọi là "ticks"). Sau một số lượng ticks nhất định, thread đang giữ GIL sẽ buộc phải giải phóng nó, để các thread khác có cơ hội chạy.
Trong các phiên bản Python gần đây (từ 3.2 trở đi), cơ chế này đã được thay đổi thành dựa trên thời gian kết hợp với các heuristic khác. Thay vì đếm số lệnh bytecode, trình thông dịch đánh giá xem thread đã giữ GIL trong bao lâu. Nếu một thread khác đang chờ, thread hiện tại sẽ bị buộc phải nhả GIL sau một khoảng thời gian cố định (ví dụ: vài mili giây). Điều này giúp cải thiện tính công bằng và giảm độ trễ cho các thread I/O-bound có thể bị bỏ qua trong cơ chế dựa trên tick cũ nếu một thread CPU-bound liên tục thực thi các lệnh bytecode nhanh.
Giải phóng Chủ động khi I/O: Khi một thread thực hiện một thao tác I/O chặn, nó sẽ chủ động giải phóng GIL trước khi thực hiện lời gọi hệ thống đó. Sau khi thao tác I/O hoàn tất, thread đó sẽ phải đợi để giành lại GIL trước khi có thể tiếp tục thực thi mã Python.
Tranh chấp (Contention): Khi nhiều thread sẵn sàng chạy (ví dụ: vừa hoàn thành I/O hoặc đang chờ sau khi bị buộc nhả GIL), chúng sẽ cạnh tranh để giành lấy GIL ngay khi nó được giải phóng. OS và trình thông dịch Python phối hợp để quyết định thread nào sẽ nhận được GIL tiếp theo.
Ý nghĩa Cơ chế: Hiểu được cơ chế kiểm tra định kỳ này (dù là tick hay time-based) cho thấy GIL không chỉ đơn thuần chặn parallelism, mà còn giới thiệu một lớp quản lý phức tạp và chi phí riêng. Nó không chỉ là "một thread chạy", mà là một quá trình chuyển đổi liên tục, nhanh chóng giữa các thread, được quản lý bởi trình thông dịch dựa trên số lệnh hoặc thời gian, tương tác với bộ lập lịch của HĐH. Cơ chế này giải thích trực tiếp tại sao các thread CPU-bound không chạy song song (chỉ một thread giữ khóa) nhưng cũng giải thích tại sao các thread I/O-bound lại cho phép concurrency (chúng giải phóng khóa khi chờ). Nó cũng làm rõ nguồn gốc của chi phí - trình thông dịch phải liên tục quản lý việc chuyển đổi khóa này.
Multiprocessing trong Python
Multiprocessing trong Python giúp đạt được Tính Song song Thực sự
Định nghĩa Process - Sự khác biệt giữa Thread và Process
Process là gì?
Một process (tiến trình) là một thực thể của một chương trình đang chạy.
Mỗi process có không gian bộ nhớ hoàn toàn độc lập của riêng nó, bao gồm mã lệnh, dữ liệu, heap, stack, và các tài nguyên hệ thống khác (như file descriptors).
Khi bạn chạy một ứng dụng Python, bạn đang khởi tạo một process.
Khác biệt Chính so với Thread:
Bộ nhớ:
Processes có không gian bộ nhớ riêng biệt.
Threads chia sẻ cùng không gian bộ nhớ của process cha chứa chúng.
Chi phí Tạo:
- Việc tạo ra một process mới thường tốn kém hơn (cả về thời gian và tài nguyên hệ thống) so với việc tạo ra một thread mới. Process cần được cấp phát không gian bộ nhớ riêng và các cấu trúc dữ liệu quản lý bởi OS.
Giao tiếp:
Giao tiếp giữa các thread (trong cùng process) rất dễ dàng vì chúng có thể truy cập trực tiếp vào cùng các biến và đối tượng trong bộ nhớ chia sẻ (dù cần cẩn thận với race conditions).
Giao tiếp giữa các process phức tạp và chậm hơn đáng kể vì chúng không chia sẻ bộ nhớ. Dữ liệu phải được truyền một cách tường minh từ process này sang process khác thông qua các cơ chế do OS hoặc thư viện cung cấp (như pipes, sockets, shared memory segments, message queues).
Đặc điểm | Thread (Luồng) | Process (Tiến trình) |
Bộ nhớ | Chia sẻ trong cùng process | Độc lập, riêng biệt |
Tạo mới | Nhanh, ít tốn kém | Chậm, tốn kém hơn |
Giao tiếp | Dễ dàng (qua bộ nhớ chia sẻ) | Phức tạp hơn (qua IPC) |
Cô lập lỗi | Kém (lỗi 1 thread có thể crash cả process) | Tốt (lỗi 1 process thường không ảnh hưởng process khác) |
GIL (CPython) | Chia sẻ 1 GIL duy nhất trong process | Mỗi process có GIL riêng |
Module multiprocessing
trong Python
Python cung cấp module multiprocessing
để tạo và quản lý các process. Đây là giải pháp mặc định và hiệu quả nhất trong thư viện chuẩn Python để đạt được tính song song (parallelism) thực sự.
multiprocessing
được thiết kế rất giống với API của threading
, giúp việc chuyển đổi giữa hai mô hình trở nên dễ dàng hơn ở mức độ mã nguồn cơ bản.Vì mỗi process được tạo bởi multiprocessing
khởi chạy một Python Interpreter hoàn toàn riêng biệt và có không gian bộ nhớ độc lập, nên mỗi process cũng có GIL riêng. Nhờ đó, nhiều process Python có thể thực thi mã bytecode Python song song thực sự trên các lõi CPU khác nhau mà không bị chặn bởi GIL của nhau.
multiprocessing
giúp giải quyết vấn đề của GIL, giúp tận dụng tối đa sức mạnh của các bộ xử lý đa lõi cho các công việc tính toán nặng bằng cách phân chia công việc cho nhiều lõi CPU xử lý đồng thời như các tác vụ CPU-bound.
CPU-bound là các tác vụ mà thời gian hoàn thành bị giới hạn bởi tốc độ xử lý của CPU như:
Các phép tính toán khoa học phức tạp (ví dụ: mô phỏng, phân tích số liệu lớn).
Xử lý hình ảnh hoặc video (ví dụ: áp dụng bộ lọc, chuyển đổi định dạng).
Mã hóa hoặc giải mã dữ liệu.
Các thuật toán học máy đòi hỏi tính toán cao.
multiprocessing
cho các tác vụ I/O-bound nhưng chi phí tạo process mới, chi phí giao tiếp liên tiến trình (IPC) thường làm cho nó kém hiệu quả hơn so với threading
hoặc asyncio
trong trường hợp cần xử lý hàng trăm hoặc hàng nghìn kết nối I/O đồng thời. Việc tạo ra hàng nghìn process là không thực tế về mặt tài nguyên hệ thống.Giao tiếp Liên tiến trình (Inter-Process Communication - IPC)
Vì các process có bộ nhớ riêng biệt, chúng cần các cơ chế đặc biệt để trao đổi dữ liệu, multiprocessing
cung cấp một số cách:
Hàng đợi (
multiprocessing.Queue
):Đây là cách phổ biến và an toàn nhất để gửi và nhận các đối tượng Python giữa các process.
Queue
là thread-safe và process-safe.Khi bạn đưa một đối tượng vào Queue từ một process,
multiprocessing
sẽ tự động pickle (tuần tự hóa - serialize) đối tượng đó, gửi dữ liệu qua một cơ chế ẩn (thường là pipe) đến process khác, nơi nó sẽ được unpickle (giải tuần tự hóa - deserialize) khi lấy ra khỏi Queue.
Đường ống (
multiprocessing.Pipe
):Tạo ra một cặp đối tượng
Connection
nối trực tiếp giữa hai process. Mỗi đầu của pipe có thể gửi (send()
) và nhận (recv()
) các đối tượng (cũng được pickle/unpickle).Pipe thường nhanh hơn Queue một chút nhưng chỉ dùng cho giao tiếp hai chiều giữa hai process cụ thể.
Bộ nhớ Chia sẻ (
Value
,Array
):Module này cũng cung cấp cách tạo các vùng nhớ chia sẻ cho các kiểu dữ liệu đơn giản (như số nguyên, số thực, mảng ký tự) thông qua
multiprocessing.Value
vàmultiprocessing.Array
.Việc này tránh được chi phí pickle/unpickle nhưng phức tạp hơn trong việc quản lý đồng bộ hóa (cần sử dụng
Lock
đi kèm) và chỉ phù hợp với các kiểu dữ liệu cơ bản.
Cần lưu ý rằng tất cả các cơ chế IPC (trừ bộ nhớ chia sẻ trực tiếp ở low-level) đều liên quan đến việc tuần tự hóa (serialization) dữ liệu (thường là dùng pickle
trong Python) ở phía gửi và giải tuần tự hóa (deserialization) ở phía nhận. Quá trình này có thể tốn kém về mặt thời gian và CPU, đặc biệt đối với các đối tượng lớn hoặc phức tạp.
Đây là một phần của sự đánh đổi khi sử dụng
multiprocessing
: Đạt được parallelism nhưng phải trả giá bằng chi phí giao tiếp khi sử dụng các cơ chế IPC (thay vì tận dụng sự tiện lợi của bộ nhớ chia sẻ trực tiếp như trong threading).
Ví dụ về việc tạo process với
Tương tự threading
, bạn có thể tạo process bằng cách tạo đối tượng multiprocessing.Process
, chỉ định hàm target
và args
, sau đó gọi .start()
và .join()
.
Có 2 cơ chế để tạo process là Fork và Spawn. Fork sử dụng chủ yếu trên các hệ thống Unix-like như Linux trong khi Spawn sử dụng cho các hệ thống Windows và MacOS.
Có thể sử dụng multiprocessing.set_start_method() với giá trị truyền vào là fork hoặc spawn để thiết lập phương thức tạo process. Từ phiên bản Python 3.8, Spawn được sử dụng mặc định trên Windows và MacOS nếu không chỉ rõ phương thức.
import multiprocessing
import time
import os
def cpu_task(name):
print(f"Process {name} (PID: {os.getpid()}): bắt đầu")
# Giả lập công việc CPU-bound
result = 0
for i in range(10**7):
result += i
print(f"Process {name} (PID: {os.getpid()}): kết thúc với kết quả {result}")
if __name__ == "__main__":
# Khối if __name__ == "__main__" là cần thiết ở khi sử dụng với phương thức spawn,
# để đảm bảo chỉ process cha tạo được process con
# thay vì process con lại có thể tạo process con nhỏ nữa gây vòng lặp vô hạn.
p1 = multiprocessing.Process(target=cpu_task, args=("A",))
p2 = multiprocessing.Process(target=cpu_task, args=("B",))
p1.start()
p2.start()
p1.join()
p2.join()
print("Tất cả các process đã hoàn thành.")
Lập trình Bất đồng bộ với asyncio
Asynchrony, Event Loop, Coroutines (async
/await
)
Lập trình Bất đồng bộ (Asynchrony): Đây là một mô hình lập trình đồng thời khác biệt:
Thay vì dựa vào nhiều thread hoặc process do OS quản lý, lập trình bất đồng bộ (trong ngữ cảnh
asyncio
) thường hoạt động trên một thread duy nhất.Ý tưởng cốt lõi là các tác vụ (tasks) sẽ tự nguyện (voluntarily) nhường quyền kiểm soát trở lại cho một bộ điều phối trung tâm (event loop) khi chúng gặp phải một hoạt động có khả năng gây chặn (thường là I/O).
Khi một tác vụ nhường quyền kiểm soát, event loop có thể chuyển sang chạy một tác vụ khác đã sẵn sàng. Điều này cho phép chương trình thực hiện công việc hữu ích khác thay vì bị chặn hoàn toàn trong khi chờ đợi I/O.
Vòng lặp Sự kiện (Event Loop): Đây là trái tim của
asyncio
. Event loop là một thực thể chạy liên tục, có vai trò:Quản lý và phân phối việc thực thi các tác vụ bất đồng bộ (coroutines).
Theo dõi các sự kiện I/O (ví dụ: dữ liệu đã đến trên socket chưa? Có thể ghi vào file được chưa?).
Gọi lại (callback) hoặc đánh thức các tác vụ tương ứng khi sự kiện I/O mà chúng đang chờ đợi xảy ra.
Ví dụ: Tượng tượng event loop như một người điều phối trong một nhà hàng chỉ có một đầu bếp (single thread). Người điều phối nhận các đơn hàng (tasks), đưa cho đầu bếp. Khi đầu bếp phải chờ một nguyên liệu (I/O wait), họ báo lại cho người điều phối và bắt đầu làm món khác. Khi nguyên liệu đến, người điều phối sẽ báo cho đầu bếp để tiếp tục món ăn đang chờ.
Coroutines (
async def
): Trongasyncio
, đơn vị cơ bản của công việc bất đồng bộ là coroutine. Đây là các hàm đặc biệt được định nghĩa bằng từ khóaasync def
. Coroutines khác với hàm thông thường ở chỗ chúng có thể tạm dừng (pause) việc thực thi tại một điểm nhất định (thường là khi chờ đợi một hoạt động I/O) và sau đó tiếp tục (resume) từ chính điểm đó sau này.Từ khóa
await
: Từ khóaawait
được sử dụng bên trong một coroutine để gọi một coroutine khác hoặc một đối tượng "awaitable" khác (thường là một hoạt động I/O bất đồng bộ). Khiawait
được gọi, nó báo hiệu rằng coroutine hiện tại có thể bị tạm dừng tại điểm này. Nó nhường quyền kiểm soát trở lại cho event loop. Event loop sẽ lo việc thực hiện hoạt động đượcawait
(ví dụ: bắt đầu yêu cầu mạng) và trong thời gian chờ đợi, nó có thể chạy các tác vụ khác. Khi hoạt động đượcawait
hoàn thành, event loop sẽ lên lịch để coroutine bị tạm dừng được tiếp tục thực thi ngay sau câu lệnhawait
. Pythonimport asyncio async def say_after(delay, what): await asyncio.sleep(delay) # await: Tạm dừng ở đây, nhường quyền cho event loop print(what) async def main(): print("Bắt đầu") # Chạy đồng thời hai coroutine mà không cần thread riêng task1 = asyncio.create_task(say_after(1, 'hello')) task2 = asyncio.create_task(say_after(2, 'world')) await task1 # Đợi task1 hoàn thành await task2 # Đợi task2 hoàn thành print("Kết thúc") # Chạy event loop để thực thi coroutine 'main' asyncio.run(main())
Tasks: Coroutines thường được bao bọc trong các đối tượng
Task
.Task
quản lý việc thực thi một coroutine, cho phép event loop theo dõi trạng thái của nó (đang chạy, bị tạm dừng, đã xong) và lên lịch chạy.asyncio.create_task()
là cách phổ biến để tạo và lên lịch chạy một coroutine trên event loop.
Đa nhiệm Hợp tác (cooperative multitasking) trong asyncio
asyncio
sử dụng đa nhiệm hợp tác (cooperative multitasking).Trong mô hình này, các tác vụ phải hợp tác bằng cách chủ động nhường quyền kiểm soát thông qua việc sử dụng
await
. Nếu một coroutine chạy một đoạn mã đồng bộ (synchronous) tốn nhiều thời gian CPU mà không hề cóawait
, nó sẽ chiếm giữ event loop và ngăn chặn tất cả các tác vụ khác chạy. Điều này có thể làm "đơ" toàn bộ ứng dụng bất đồng bộ.Khác với
threading
sử dụng đa nhiệm phân chia thời gian (preemptive) của OS, OS tự quyết định khi nào dừng thread này và chạy thread khácasyncio
đạt được concurrency trên một thread duy nhất. Nó không sử dụng nhiều lõi CPU để thực thi mã Python song song. Sức mạnh của nó nằm ở khả năng quản lý hiệu quả số lượng rất lớn (hàng trăm, hàng nghìn, thậm chí hàng chục nghìn) các kết nối hoặc tác vụ I/O đồng thời mà không cần đến chi phí tài nguyên của hàng ngàn thread hay process.Ví dụ điển hình:
Xây dựng các máy chủ mạng hiệu năng cao (web servers, API backends, chat servers) có khả năng xử lý nhiều client cùng lúc.
Viết các client mạng cần thực hiện nhiều yêu cầu đồng thời (ví dụ: web crawler/scraper tải nhiều trang cùng lúc).
Quản lý các nhóm kết nối cơ sở dữ liệu (database connection pools) và thực hiện các truy vấn đồng thời.
Các hệ thống thời gian thực cần phản ứng nhanh chóng với nhiều nguồn sự kiện đầu vào.
So với
threading
hoặcmultiprocessing
,asyncio
có chi phí tài nguyên cực kỳ thấp cho mỗi tác vụ đồng thời. Việc tạo mộtTask
trongasyncio
tốn ít bộ nhớ và CPU hơn nhiều so với việc tạo một thread hoặc process hoàn chỉnh. Điều này làm choasyncio
trở thành lựa chọn lý tưởng khi cần đạt được mức độ đồng thời I/O rất cao mà không làm cạn kiệt tài nguyên hệ thống.
Cơ chế Hoạt động Bên trong: Event Loop và Sự kiện I/O
Nguyên lý hoạt động: Event Loop của
asyncio
thường dựa vào các cơ chế thông báo sự kiện I/O hiệu quả do OS cung cấp. Các cơ chế này cho phép Event Loop theo dõi trạng thái của nhiều file descriptor (đại diện cho socket mạng, file, pipes,...) cùng một lúc mà không cần phải liên tục kiểm tra từng cái một (polling). Các cơ chế phổ biến bao gồm như:epoll
(trên Linux),kqueue
(trên macOS và các hệ thống BSD khác),select
(cũ hơn), IOCP (I/O Completion Ports) (trên Windows)Cơ chế Callback/Future: Khi một coroutine thực hiện
await
trên một hoạt động I/O (ví dụ:await
reader.read
(100
) để đọc dữ liệu từ socket),asyncio
sẽ làm những việc sau:Nó yêu cầu OS bắt đầu thao tác I/O (ví dụ: yêu cầu đọc từ socket).
Nó đăng ký (register) file descriptor tương ứng (socket) với bộ theo dõi I/O của event loop (ví dụ:
epoll
), yêu cầu được thông báo khi file descriptor đó sẵn sàng để đọc.Nó tạm dừng coroutine hiện tại (đánh dấu
Task
là đang chờ).Event loop tiếp tục chạy, xử lý các sự kiện khác hoặc chạy các
Task
khác đã sẵn sàng.Khi OS thông báo cho event loop rằng dữ liệu đã có trên socket (thông qua
epoll
chẳng hạn), event loop sẽ:Đánh dấu
Task
đang chờ đợi trên socket đó là sẵn sàng để chạy (ready).Trong vòng lặp tiếp theo của mình, event loop sẽ chọn
Task
đó và tiếp tục (resume) thực thi coroutine từ ngay sau câu lệnhawait
đã tạm dừng nó.
Ý nghĩa Cơ chế: Việc dựa vào các cơ chế thông báo sự kiện I/O của HĐH như
epoll
haykqueue
chính là chìa khóa cho hiệu quả củaasyncio
. Thay vì lãng phí CPU để liên tục hỏi "Đã có dữ liệu chưa?" cho hàng nghìn kết nối, event loop chỉ cần nói với HĐH: "Hãy cho tôi biết khi nào có dữ liệu trên bất kỳ kết nối nào trong số này". Điều này cho phép một thread duy nhất quản lý hiệu quả hàng nghìn kết nối đang chờ I/O, chỉ tiêu tốn CPU khi thực sự có việc cần làm (xử lý dữ liệu đến hoặc đi). Nó kết nối trực tiếp từ khóaawait
bậc cao với việc xử lý sự kiện I/O bậc thấp của HĐH, làm sáng tỏ cách đa nhiệm hợp tác hoạt động hiệu quả trong thực tế cho I/O.
So sánh các Mô hình Đồng thời trong Python
Sau khi đã tìm hiểu chi tiết về từng mô hình, việc so sánh trực tiếp 3 mô hình Threading vs. Multiprocessing vs. Asyncio sẽ giúp củng cố hiểu biết và làm rõ hơn sự khác biệt, ưu nhược điểm của chúng.
Cơ chế Hoạt động
Threading (
threading
):Sử dụng nhiều thread của OS bên trong một process duy nhất.
Các thread chia sẻ cùng không gian bộ nhớ.
Concurrency được quản lý bởi bộ lập lịch của OS (đa nhiệm phân chia thời gian - preemptive).
Bị giới hạn bởi GIL đối với các tác vụ CPU-bound trong CPython (không có parallelism thực sự cho mã Python).
Multiprocessing (
multiprocessing
):Sử dụng nhiều process của OS, mỗi process có Interpreter và không gian bộ nhớ riêng.
Đạt được parallelism thực sự trên các lõi CPU khác nhau.
Mỗi process có GIL riêng, do đó không bị giới hạn bởi GIL của process khác.
Concurrency cũng được quản lý bởi bộ lập lịch của OS (preemptive).
Yêu cầu Giao tiếp Liên tiến trình (IPC) để trao đổi dữ liệu giữa các process.
Asynchrony (
asyncio
):Thường sử dụng một thread duy nhất và một event loop.
Quản lý concurrency thông qua đa nhiệm hợp tác (cooperative): các coroutine tự nguyện nhường quyền kiểm soát bằng
await
.Không cung cấp parallelism cho việc thực thi mã Python trên nhiều lõi.
Tập trung vào việc xử lý hiệu quả số lượng lớn các tác vụ I/O-bound non-blocking.
Ưu điểm và Nhược điểm
Threading:
Ưu điểm:
Tương đối đơn giản để bắt đầu cho các tác vụ cơ bản.
Chia sẻ dữ liệu dễ dàng qua bộ nhớ chung.
Rất tốt cho việc che giấu độ trễ của các tác vụ I/O-bound (cải thiện thông lượng và tính phản hồi).
Nhược điểm:
GIL là nút cổ chai lớn cho tác vụ CPU-bound trong CPython.
Rủi ro cao về race conditions nếu không quản lý cẩn thận việc truy cập dữ liệu chia sẻ (yêu cầu sử dụng locks, etc.).
Số lượng thread có thể tạo bị giới hạn bởi tài nguyên OS và chi phí chuyển đổi ngữ cảnh có thể trở nên đáng kể.
Multiprocessing:
Ưu điểm:
Cung cấp parallelism thực sự cho tác vụ CPU-bound, tận dụng đa lõi.
Vượt qua hoàn toàn giới hạn của GIL.
Cô lập lỗi tốt hơn (một process bị lỗi thường không ảnh hưởng đến các process khác).
Nhược điểm:
Chi phí tài nguyên cao: Tốn nhiều bộ nhớ hơn cho mỗi process, thời gian khởi tạo process lâu hơn.
IPC phức tạp và tốn kém: Truyền dữ liệu giữa các process yêu cầu serialization - deserialization, chậm hơn so với truy cập bộ nhớ chia sẻ.
Quản lý process có thể phức tạp hơn.
Asyncio:
Ưu điểm:
Hiệu quả cực cao cho việc xử lý hàng nghìn hoặc hàng triệu kết nối/tác vụ I/O đồng thời.
Chi phí tài nguyên rất thấp cho mỗi tác vụ (ít bộ nhớ, ít chi phí chuyển đổi ngữ cảnh so với thread).
Kiểm soát luồng thực thi rõ ràng hơn thông qua
async
/await
.
Nhược điểm:
Yêu cầu thay đổi mô hình lập trình (phải sử dụng
async
/await
, suy nghĩ theo hướng bất đồng bộ).Không phù hợp cho việc tăng tốc các tác vụ CPU-bound (vì chạy trên một thread).
Một đoạn mã đồng bộ (synchronous) chạy lâu bên trong một coroutine mà không
await
sẽ chặn toàn bộ event loop, ảnh hưởng đến tất cả các tác vụ khác.Cần hệ sinh thái thư viện hỗ trợ (các thư viện I/O phải là phiên bản
async
).
Ảnh hưởng của GIL
Threading: Bị ảnh hưởng nghiêm trọng đối với tác vụ CPU-bound (ngăn chặn parallelism). Ít ảnh hưởng hơn đối với tác vụ I/O-bound (vì GIL thường được giải phóng khi chờ I/O).
Multiprocessing: Không bị ảnh hưởng (mỗi process có GIL riêng). Đây là cách chính để "né" GIL cho CPU-bound parallelism.
Asyncio: Không bị ảnh hưởng trực tiếp (vì thường chạy trên một thread). Tuy nhiên, một tác vụ đồng bộ chạy lâu trong
asyncio
có thể gây ra hiệu ứng "chặn" tương tự như GIL đối với thread duy nhất đó, làm dừng tất cả các tác vụ khác trên cùng event loop.
Mức độ Phức tạp và Chi phí Tài nguyên (Bộ nhớ, CPU)
Threading:
Bộ nhớ: Trung bình (mỗi thread cần stack riêng và tài nguyên quản lý của HĐH).
CPU: Chi phí chuyển đổi ngữ cảnh của HĐH, chi phí tranh chấp GIL (nếu có).
Độ phức tạp: Khái niệm cơ bản dễ, nhưng quản lý đồng bộ hóa an toàn (locks, etc.) có thể rất phức tạp.
Multiprocessing:
Bộ nhớ: Cao (mỗi process là một bản sao gần như hoàn chỉnh, tốn nhiều RAM).
CPU: Chi phí tạo process cao, chi phí IPC (serialization/deserialization) có thể đáng kể.
Độ phức tạp: Cao (quản lý process, thiết kế IPC, xử lý serialization).
Asyncio:
Bộ nhớ: Rất thấp (chi phí cho mỗi
Task
nhỏ hơn nhiều so với thread/process).CPU: Chi phí quản lý event loop và chuyển đổi giữa các coroutine thấp khi chờ I/O. Có thể tốn CPU nếu mã trong coroutine tính toán nhiều.
Độ phức tạp: Trung bình đến Cao (cần hiểu mô hình
async
/await
, event loop, quản lý callback/future, nguy cơ chặn event loop).
Bảng Tóm tắt So sánh
Bảng dưới đây tóm tắt các điểm khác biệt chính giữa ba phương pháp:
Đặc điểm | Threading | Multiprocessing | Asynchrony |
Loại Đa nhiệm | Phân chia thời gian (Preemptive) | Phân chia thời gian (Preemptive) | Hợp tác (Cooperative) |
Parallelism (CPython) | Không (cho CPU-bound) | Có (True Parallelism) | Không |
Trường hợp sử dụng chính | I/O-bound (vừa phải) | CPU-bound | I/O-bound (số lượng rất lớn) |
Giới hạn GIL (CPython) | Có (nghiêm trọng cho CPU-bound) | Không (mỗi process có GIL riêng) | Không trực tiếp (nhưng có thể bị chặn bởi mã sync) |
Chia sẻ Bộ nhớ | Dễ dàng (chung bộ nhớ) | Khó khăn (qua IPC) | Không áp dụng trực tiếp (chạy trên 1 thread) |
Chi phí Tài nguyên (Bộ nhớ) | Trung bình | Cao | Rất thấp |
Chi phí Tài nguyên (CPU) | Chuyển đổi ngữ cảnh, tranh chấp GIL | Tạo process, IPC (serialization) | Quản lý event loop (thấp khi chờ I/O) |
Giao tiếp | Bộ nhớ chia sẻ | IPC (Queues, Pipes) | Trong cùng process (biến, queues asyncio) |
Mô hình Lập trình | Hàm thông thường | Hàm thông thường | async /await , coroutines |
Độ phức tạp | Trung bình (cao nếu cần đồng bộ hóa phức tạp) | Cao (IPC, quản lý process) | Trung bình - Cao (mô hình async, event loop) |
Lựa chọn Phương pháp Phù hợp
Sau khi đã khám phá các cơ chế, ưu nhược điểm của từng phương pháp, câu hỏi quan trọng là: Khi nào nên sử dụng cái nào? Lựa chọn đúng kỹ thuật concurrency là rất quan trọng để xây dựng các ứng dụng Python hiệu quả và đáp ứng tốt.
Tổng hợp Kiến thức
Vấn đề Cốt lõi: Làm thế nào để chương trình Python thực hiện được nhiều việc hơn cùng một lúc, đặc biệt là khi đối mặt với các tác vụ chờ đợi (I/O) hoặc cần tận dụng sức mạnh tính toán của CPU đa lõi.
Các Giải pháp Chính:
Threading: Tốt cho việc che giấu độ trễ của các tác vụ I/O-bound, giúp ứng dụng phản hồi tốt hơn bằng cách cho phép các công việc khác chạy trong khi chờ đợi. Không giúp tăng tốc tác vụ CPU-bound trong CPython do GIL.
Multiprocessing: Giải pháp để đạt được tính song song thực sự cho các tác vụ CPU-bound, vượt qua giới hạn GIL bằng cách sử dụng các process riêng biệt. Tốn kém hơn về tài nguyên và yêu cầu IPC.
Asyncio: Tối ưu cho việc xử lý số lượng cực lớn các tác vụ I/O-bound một cách hiệu quả về tài nguyên, sử dụng mô hình hợp tác trên một thread duy nhất. Yêu cầu thay đổi cách viết mã (
async
/await
).
Điểm Mấu chốt: Luôn nhớ sự khác biệt giữa Concurrency (Đồng thời) và Parallelism (Song song), và vai trò trung tâm của GIL trong việc định hình cách các mô hình này hoạt động trong CPython.
Các Yếu tố Cần Cân nhắc khi chọn phương pháp phù hợp
Để chọn phương pháp phù hợp, hãy xem xét các yếu tố sau theo thứ tự ưu tiên:
Xác định Loại Tác vụ Chính: Đây là yếu tố quan trọng nhất.
Công việc của bạn chủ yếu là chờ đợi I/O? (Ví dụ: Gửi nhiều yêu cầu API, truy vấn nhiều cơ sở dữ liệu, đọc/ghi nhiều file, quản lý nhiều kết nối mạng đồng thời).
Nếu số lượng tác vụ đồng thời không quá lớn (vài chục đến vài trăm) và bạn muốn sự đơn giản tương đối: Threading là một lựa chọn tốt.
Nếu bạn cần xử lý số lượng rất lớn (hàng nghìn trở lên) các tác vụ I/O đồng thời và muốn hiệu quả tài nguyên tối đa: Asyncio là lựa chọn hàng đầu.
Công việc của bạn chủ yếu là tính toán nặng, tốn CPU? (Ví dụ: Xử lý dữ liệu lớn, tính toán khoa học, xử lý ảnh/video).
- Để tận dụng các lõi CPU và đạt được parallelism thực sự: sử dụng Multiprocessing.
Công việc của bạn là sự kết hợp của cả hai? (Ví dụ: Một web server cần xử lý yêu cầu (I/O) và thực hiện một số tính toán (CPU) cho mỗi yêu cầu).
Đây là trường hợp phức tạp hơn. Bạn có thể cần kết hợp các phương pháp. Ví dụ: Sử dụng
asyncio
hoặcthreading
để xử lý các kết nối I/O, và khi cần thực hiện một tác vụ CPU-bound nặng, hãy gửi tác vụ đó đến một pool các process worker được quản lý bởimultiprocessing
(ví dụ thông quaconcurrent.futures.ProcessPoolExecutor
hoặcmultiprocessing.Pool
).Thư viện
concurrent.futures
cung cấp một giao diện bậc cao hơn (ThreadPoolExecutor
vàProcessPoolExecutor
) có thể giúp đơn giản hóa việc quản lý cả thread và process pool.
Xem xét Quy mô (Scale):
Vài tác vụ đồng thời: Threading có thể là đủ và đơn giản nhất.
Vài chục đến vài trăm tác vụ I/O: Threading hoặc Asyncio đều khả thi.
Hàng nghìn hoặc nhiều hơn tác vụ I/O: Asyncio thường hiệu quả hơn nhiều về tài nguyên.
Cần tận dụng tối đa tất cả các lõi CPU cho tính toán: Multiprocessing là cần thiết.
Cân nhắc đến GIL (nếu dùng CPython):
- Nhắc lại: Đừng mong đợi
threading
tăng tốc mã Python thuần túy CPU-bound trên đa lõi. Hãy chọnmultiprocessing
cho mục đích đó.
- Nhắc lại: Đừng mong đợi
Đánh giá Độ phức tạp và Sự quen thuộc:
Bạn có cảm thấy thoải mái với cú pháp
async
/await
và tư duy bất đồng bộ không? Nếu có,asyncio
rất mạnh mẽ cho I/O.Việc quản lý các process riêng biệt và giao tiếp qua IPC có khả thi cho dự án của bạn không? Nếu cần parallelism cho CPU,
multiprocessing
là lựa chọn, dù phức tạp hơn.Bạn chỉ cần che giấu độ trễ I/O một cách đơn giản?
Threading
thường là điểm khởi đầu dễ tiếp cận nhất.
Luồng Quyết định Đơn giản (Tham khảo):
1. Bạn có cần tăng tốc tác vụ CPU-bound bằng cách dùng nhiều lõi không?
|
+-- CÓ --> Sử dụng Multiprocessing (`multiprocessing`, `concurrent.futures.ProcessPoolExecutor`)
|
+-- KHÔNG (Chủ yếu là I/O-bound hoặc không cần parallelism) --> Đi tiếp bước 2
2. Tác vụ chính có phải là I/O-bound không?
|
+-- CÓ --> Đi tiếp bước 3
|
+-- KHÔNG (Không rõ ràng hoặc không phải I/O/CPU chính) --> Xem xét lại yêu cầu / Chạy tuần tự có thể đủ / Cân nhắc Threading cho các tác vụ nền đơn giản.
3. Bạn có cần xử lý số lượng rất lớn (> hàng trăm/nghìn) kết nối/tác vụ I/O đồng thời và ưu tiên hiệu quả tài nguyên không?
|
+-- CÓ --> Sử dụng Asynchrony (`asyncio`)
|
+-- KHÔNG (Số lượng vừa phải, ưu tiên sự đơn giản tương đối) --> Sử dụng Threading (`threading`, `concurrent.futures.ThreadPoolExecutor`)
cProfile
hoặc các thư viện profiling khác để xác định xem chương trình của bạn thực sự đang chậm ở đâu (đâu là nút cổ chai). Đôi khi, tối ưu hóa không cần thiết hoặc áp dụng sai chỗ có thể làm phức tạp mã nguồn mà không mang lại lợi ích đáng kể.Tìm hiểu Thêm:
concurrent.futures
: Khám phá module này, nó cung cấp một API bậc cao, thống nhất hơn để làm việc với cả thread pool và process pool.Thư viện Async Nâng cao: Nếu đi sâu vào
asyncio
, hãy xem xét các thư viện nhưhttpx
(client HTTP async),aiohttp
(web framework/client async),Trio
(một thư viện async khác với cách tiếp cận hơi khácasyncio
).Đồng bộ hóa Nâng cao: Nếu sử dụng
threading
hoặcmultiprocessing
nhiều, hãy tìm hiểu kỹ hơn về các cơ chế đồng bộ hóa (Locks, Semaphores, Events, Conditions, Queues) để tránh race conditions và deadlocks.
Hiểu và áp dụng đúng các kỹ thuật concurrency sẽ giúp bạn viết các ứng dụng Python mạnh mẽ, hiệu quả và đáp ứng tốt hơn. Quá trình này đòi hỏi thời gian và thực hành, nhưng những kiến thức nền tảng bạn đã tìm hiểu ở đây là bước khởi đầu vững chắc. Chúc bạn thành công trên con đường học tập của mình!
Subscribe to my newsletter
Read articles from Ha Ngoc Hieu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
