Giới thiệu về tạo thread trong Python

Bạn có bao giờ gặp tình huống chương trình Python chạy chậm khi phải xử lý nhiều tác vụ cùng lúc không? Đây chính là lúc bạn cần tìm hiểu về thread (luồng) – một kỹ thuật lập trình mạnh mẽ giúp chương trình thực hiện đa nhiệm hiệu quả hơn.
Khi làm việc với Python, việc tải file từ internet, xử lý dữ liệu hay thao tác với cơ sở dữ liệu thường mất nhiều thời gian chờ đợi. Thay vì để chương trình “ngồi không” trong thời gian này, thread cho phép bạn chạy nhiều tác vụ đồng thời, tận dụng tối đa tài nguyên hệ thống.
Trong bài viết này, tôi sẽ hướng dẫn bạn từ khái niệm cơ bản đến cách áp dụng thread thực tế. Chúng ta sẽ cùng tìm hiểu cách tạo thread, quản lý nhiều luồng, và những lưu ý quan trọng khi sử dụng. Đặc biệt, bạn sẽ có các ví dụ code chi tiết để thực hành ngay.
Nội dung bài viết bao gồm khái niệm thread và phân biệt với process, lợi ích cũng như hạn chế, cách tạo thread bằng module threading, ví dụ thực tế và những vấn đề thường gặp cần tránh.
Thread là gì? Phân biệt thread và process trong Python

Khái niệm thread và ứng dụng thực tiễn
Thread hay luồng là một đơn vị thực thi nhỏ bên trong một chương trình (process). Hình dung đơn giản, nếu process là một nhà máy, thì thread giống như các dây chuyền sản xuất hoạt động song song trong cùng nhà máy đó.
Trong Python, thread đặc biệt hữu ích cho các tác vụ I/O-bound – nghĩa là những hoạt động phải chờ đợi như đọc file, gửi request HTTP, hay truy vấn database. Ví dụ, khi bạn cần tải 10 file từ internet, thay vì tải lần lượt từng file (mất 10 giây), thread cho phép tải đồng thời cả 10 file (chỉ mất khoảng 1-2 giây).
Các ứng dụng phổ biến của thread bao gồm: xây dựng web server đáp ứng nhiều client cùng lúc, tải dữ liệu từ nhiều nguồn, xử lý đồng thời các tác vụ nền trong ứng dụng desktop.
Xem thêm hướng dẫn chi tiết về Ứng dụng của Python để hiểu rõ hơn về các lĩnh vực sử dụng thread hiệu quả.
Phân biệt thread và process
Process là một chương trình độc lập chạy trong bộ nhớ, có không gian địa chỉ riêng biệt. Trong khi đó, thread chia sẻ cùng không gian bộ nhớ với các thread khác trong cùng process.
Ưu điểm của thread so với process: tốc độ tạo nhanh hơn, tiêu tốn ít tài nguyên hơn, dễ dàng chia sẻ dữ liệu giữa các thread. Tuy nhiên, thread cũng có nhược điểm: nếu một thread gặp lỗi nghiêm trọng có thể làm sập toàn bộ chương trình, trong khi process thì không.
Bạn nên chọn thread khi cần xử lý I/O đồng thời, chia sẻ dữ liệu thường xuyên giữa các tác vụ. Ngược lại, process phù hợp khi cần tính toán CPU-intensive, đòi hỏi độ ổn định cao hoặc chạy trên nhiều máy khác nhau.
Lợi ích và hạn chế khi dùng thread trong Python

Ưu điểm khi sử dụng thread
Thread mang lại nhiều lợi ích thiết thực cho lập trình viên Python. Đầu tiên là khả năng tăng tốc đáng kể cho các tác vụ I/O-bound. Khi chương trình phải chờ đợi phản hồi từ server, đọc file disk hay nhập liệu từ người dùng, thread khác có thể tiếp tục thực hiện công việc.
Lợi ích thứ hai là khả năng chia sẻ dữ liệu dễ dàng. Các thread trong cùng process có thể truy cập chung biến, list, dictionary mà không cần cơ chế truyền dữ liệu phức tạp như process. Điều này giúp xây dựng ứng dụng có nhiều thành phần tương tác với nhau một cách thuận tiện.
Thread còn giúp cải thiện trải nghiệm người dùng. Trong ứng dụng desktop, bạn có thể dùng thread để thực hiện tác vụ nặng ở background trong khi giao diện vẫn phản hồi mượt mà.
Tìm hiểu thêm List trong Python để hiểu cách quản lý dữ liệu dùng chung trong thread hiệu quả.
Nhược điểm và giới hạn
Python có một đặc điểm quan trọng gọi là GIL (Global Interpreter Lock) – cơ chế đảm bảo chỉ có một thread Python chạy tại một thời điểm. Điều này có nghĩa thread trong Python không thể tận dụng hoàn toàn sức mạnh của CPU đa nhân cho tính toán thuần túy.
GIL làm cho thread không hiệu quả với CPU-bound tasks – những tác vụ cần tính toán nhiều như xử lý ảnh, thuật toán machine learning. Trong trường hợp này, multiprocessing sẽ phù hợp hơn.
Ngoài ra, thread cũng tiềm ẩn rủi ro về đồng bộ dữ liệu. Khi nhiều thread truy cập cùng một biến, có thể xảy ra race condition – hiện tượng kết quả phụ thuộc vào thời điểm thực thi. Deadlock cũng là vấn đề cần chú ý khi các thread chờ đợi lẫn nhau tạo thành chu trình vô tận.
Cách tạo thread trong Python với module threading

Tạo thread bằng hàm target
Cách đơn giản nhất để tạo thread trong Python là sử dụng lớp threading.Thread với tham số target. Đây là phương pháp phù hợp khi bạn có sẵn hàm cần chạy song song.
import threading
import time
def print_numbers():
for i in range(5):
print(f"Số: {i}")
time.sleep(1)
def print_letters():
for letter in ['A', 'B', 'C', 'D', 'E']:
print(f"Chữ: {letter}")
time.sleep(1)
# Tạo thread
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)
# Khởi chạy thread
thread1.start()
thread2.start()
# Đợi thread hoàn thành
thread1.join()
thread2.join()
print("Hoàn thành!")
Trong ví dụ này, hai hàm print_numbers
và print_letters
chạy đồng thời thay vì tuần tự. Phương thức start()
khởi chạy thread, còn join()
đảm bảo chương trình chính chờ thread hoàn thành.
Để hiểu rõ hơn về các Hàm trong Python và cách sử dụng, bạn có thể tham khảo bài viết chuyên sâu về hàm.
Tạo thread bằng class kế thừa Thread
Cách tiếp cận thứ hai là tạo class con kế thừa từ threading.Thread và override phương thức run()
. Phương pháp này linh hoạt hơn, cho phép đóng gói logic và dữ liệu trong cùng một đối tượng.
import threading
import time
class MyThread(threading.Thread):
def __init__(self, name, delay):
threading.Thread.__init__(self)
self.name = name
self.delay = delay
def run(self):
print(f"Bắt đầu thread: {self.name}")
for i in range(3):
print(f"{self.name}: Lần {i+1}")
time.sleep(self.delay)
print(f"Kết thúc thread: {self.name}")
# Tạo và chạy thread
thread1 = MyThread("Worker-1", 1)
thread2 = MyThread("Worker-2", 2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
Class này cho phép tùy chỉnh hành vi của thread, truyền tham số qua constructor và quản lý state dễ dàng hơn.
Ví dụ thực tế về sử dụng nhiều thread trong Python

Chạy song song nhiều thread cùng lúc
Hãy xem một ví dụ thực tế về việc tải dữ liệu từ nhiều URL đồng thời. Thay vì tải tuần tự từng URL (chậm), chúng ta sẽ dùng thread để tải songs song:
import threading
import requests
import time
def fetch_data(url, results, index):
try:
print(f"Đang tải: {url}")
response = requests.get(url, timeout=5)
results[index] = f"URL {index}: {response.status_code}"
print(f"Hoàn thành: {url}")
except Exception as e:
results[index] = f"URL {index}: Lỗi - {str(e)}"
# Danh sách URL cần tải
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/1",
"https://httpbin.org/status/200"
]
# Lưu kết quả
results = [None] * len(urls)
threads = []
start_time = time.time()
# Tạo và chạy thread
for i, url in enumerate(urls):
thread = threading.Thread(target=fetch_data, args=(url, results, i))
threads.append(thread)
thread.start()
# Chờ tất cả thread hoàn thành
for thread in threads:
thread.join()
end_time = time.time()
print("Kết quả:")
for result in results:
print(result)
print(f"Tổng thời gian: {end_time - start_time:.2f} giây")
Truyền tham số cho thread và quản lý kết quả
Để truyền tham số cho thread, bạn có thể sử dụng tham số args
(tuple) hoặc kwargs
(dictionary) của threading.Thread. Việc thu thập kết quả từ thread cần cẩn thận vì các thread chạy bất đồng bộ.
import threading
import time
# Sử dụng Lock để đồng bộ
result_lock = threading.Lock()
shared_results = []
def calculate_sum(numbers, thread_name):
total = sum(numbers)
time.sleep(1) # Giả lập tính toán phức tạp
# Sử dụng lock khi truy cập biến chung
with result_lock:
shared_results.append(f"{thread_name}: {total}")
print(f"Thread {thread_name} hoàn thành: {total}")
# Dữ liệu đầu vào
data = [
([1, 2, 3, 4, 5], "A"),
([6, 7, 8, 9, 10], "B"),
([11, 12, 13, 14, 15], "C")
]
threads = []
# Tạo thread với tham số
for numbers, name in data:
thread = threading.Thread(target=calculate_sum, args=(numbers, name))
threads.append(thread)
thread.start()
# Đợi hoàn thành
for thread in threads:
thread.join()
print("Tất cả kết quả:")
for result in shared_results:
print(result)
Trong ví dụ này, threading.Lock() đảm bảo chỉ một thread truy cập shared_results
tại một thời điểm, tránh xung đột dữ liệu. Để hiểu thêm về kiểu dữ liệu và cách quản lý, bạn nên xem qua bài viết về Kiểu dữ liệu trong Python.
Các vấn đề thường gặp khi sử dụng thread

Thread không chạy hoặc bị treo
Một vấn đề phổ biến là thread không chạy như mong đợi. Nguyên nhân thường gặp bao gồm: quên gọi start()
, sử dụng run()
thay vì start()
, hoặc thread bị exception mà không được xử lý.
import threading
import time
def problematic_function():
print("Thread bắt đầu")
# Lỗi: chia cho 0
result = 10 / 0
print("Thread kết thúc")
# Cách xử lý lỗi trong thread
def safe_function():
try:
print("Thread an toàn bắt đầu")
result = 10 / 0
print("Thread an toàn kết thúc")
except Exception as e:
print(f"Lỗi trong thread: {e}")
# Thread có lỗi sẽ chết thầm lặng
thread1 = threading.Thread(target=problematic_function)
thread1.start()
thread1.join()
print("---")
# Thread xử lý lỗi tốt hơn
thread2 = threading.Thread(target=safe_function)
thread2.start()
thread2.join()
Luôn bao bọc code trong thread bằng try-except để xử lý lỗi gracefully.
Lỗi deadlock và race condition
Race condition xảy ra khi nhiều thread truy cập cùng dữ liệu mà không có đồng bộ hóa. Deadlock là tình huống các thread chờ đợi lẫn nhau mãi mãi.
import threading
import time
# Ví dụ về race condition
counter = 0
counter_lock = threading.Lock()
def increment_counter():
global counter
for _ in range(100000):
# Không dùng lock - có thể bị race condition
with counter_lock: # Dùng lock để tránh race condition
counter += 1
# Tạo nhiều thread cùng tăng counter
threads = []
for i in range(5):
thread = threading.Thread(target=increment_counter)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print(f"Giá trị counter: {counter}") # Kỳ vọng: 500000
Sử dụng Lock, RLock, Semaphore để đồng bộ hóa và tránh các vấn đề này.
Thực hành tốt khi làm việc với thread

Khi làm việc với thread, có một số nguyên tắc quan trọng bạn cần tuân thủ. Đầu tiên, luôn sử dụng join()
để đảm bảo chương trình chính chờ thread con hoàn thành trước khi kết thúc. Điều này tránh tình huống thread chưa xong việc nhưng chương trình đã thoát.
Thứ hai, tránh truy cập đồng thời vào biến chung mà không có cơ chế bảo vệ. Sử dụng Lock, RLock hoặc các primitive đồng bộ khác khi cần thiết. Nguyên tắc là “shared mutable state” luôn cần được bảo vệ.
Thứ ba, hiểu rõ khi nào nên dùng thread và khi nào không. Thread phù hợp với I/O-bound tasks như network requests, file operations, database queries. Đối với CPU-bound tasks, hãy cân nhắc sử dụng multiprocessing module thay thế.
Cuối cùng, đặt tên thread có ý nghĩa và log thông tin debug khi cần. Điều này giúp việc troubleshooting dễ dàng hơn khi gặp vấn đề. Sử dụng context manager (with
statement) khi có thể để đảm bảo resource được giải phóng đúng cách.
Kết luận

Thread là một công cụ mạnh mẽ giúp Python xử lý đa nhiệm hiệu quả, đặc biệt hữu ích cho các tác vụ I/O-bound như tải dữ liệu từ internet, đọc file, hay truy vấn database. Thông qua bài viết này, bạn đã nắm được cách tạo thread bằng hai phương pháp chính: sử dụng hàm target và kế thừa class Thread.
Hiểu đúng về lợi ích và hạn chế của thread giúp bạn áp dụng đúng lúc, đúng chỗ. Nhớ rằng thread không phải giải pháp vạn năng – GIL của Python hạn chế hiệu suất cho CPU-bound tasks. Trong những trường hợp này, multiprocessing sẽ là lựa chọn tốt hơn.
Các vấn đề như race condition, deadlock hay thread bị treo đều có thể tránh được nếu bạn áp dụng đúng thực hành tốt: sử dụng Lock để bảo vệ dữ liệu chung, xử lý exception trong thread, và quản lý lifecycle của thread cẩn thận.
Bây giờ đã có kiến thức cơ bản, hãy thử áp dụng thread vào dự án thực tế của bạn. Bắt đầu từ những tác vụ đơn giản như tải nhiều file đồng thời, rồi dần khám phá những khả năng mạnh mẽ khác của threading. Đừng ngần ngại thử nghiệm và học hỏi từ thực hành – đây chính là cách tốt nhất để thành thục kỹ thuật lập trình đa luồng này.
Chia sẻ Tài liệu học Python