2.1 Mô-đun threading
Đa luồng trong Python là cách thực thi nhiều luồng (threads) cùng lúc, giúp sử dụng tài nguyên CPU hiệu quả hơn, đặc biệt cho các thao tác I/O hoặc các nhiệm vụ khác có thể thực hiện song song.
Những khái niệm cơ bản về đa luồng trong Python:
Luồng — là đơn vị thực thi nhỏ nhất có thể chạy song song với các luồng khác trong cùng một tiến trình. Tất cả các luồng trong một tiến trình chia sẻ bộ nhớ chung, cho phép trao đổi dữ liệu giữa các luồng.
Tiến trình — là một instance của chương trình đang chạy trong hệ điều hành với không gian địa chỉ và tài nguyên riêng. Không giống như luồng, các tiến trình cách ly nhau và trao đổi dữ liệu thông qua giao tiếp liên tiến trình (IPC).
GIL — là cơ chế trong trình thông dịch Python ngăn cản việc thực thi nhiều luồng Python cùng lúc. GIL đảm bảo an toàn cho việc thực thi mã Python, nhưng hạn chế hiệu suất của các chương trình đa luồng trên bộ vi xử lý đa nhân.
Quan trọng! Cần lưu ý rằng do Global Interpreter Lock (GIL) đa luồng trong Python có thể không cung cấp sự gia tăng đáng kể về hiệu suất cho các nhiệm vụ tính toán chuyên sâu, vì GIL ngăn cản việc thực thi đồng thời nhiều luồng Python trên bộ vi xử lý đa nhân.
Mô-đun threading
Mô-đun threading trong Python cung cấp giao diện mức cao để làm việc với luồng. Nó cho phép tạo và quản lý luồng, đồng bộ hóa và tổ chức tương tác giữa các luồng. Hãy cùng xem xét các thành phần chính và chức năng của mô-đun này chi tiết hơn.
Các thành phần chính của mô-đun threading
Các thực thể để làm việc với luồng:
-
Thread— lớp cơ bản để tạo và quản lý các luồng. -
Timer— bộ đếm thời gian để thực thi hàm sau một khoảng thời gian xác định. -
ThreadLocal— cho phép tạo dữ liệu cục bộ cho luồng.
Cơ chế đồng bộ hóa luồng:
-
Lock— nguyên thủy đồng bộ hóa để ngăn chặn truy cập đồng thời đến tài nguyên chung. -
Condition— biến điều kiện cho đồng bộ hóa luồng phức tạp hơn. Event— nguyên thủy để thông báo giữa các luồng.-
Semaphore— nguyên thủy để giới hạn số lượng luồng có thể chạy đồng thời một đoạn mã nhất định. -
Barrier— đồng bộ hóa số lượng luồng xác định, chặn chúng cho đến khi tất cả đạt đến rào cản.
Dưới đây mình sẽ nói về 3 lớp để làm việc với luồng, cơ chế đồng bộ hóa luồng bạn sẽ không cần tới trong thời gian tới.
2.2 Lớp Thread
Lớp Thread — lớp cơ bản để tạo và quản lý các luồng. Nó có 4 phương thức chính:
start(): Bắt đầu thực thi luồng.-
join(): Luồng hiện tại bị tạm dừng và đợi hoàn thành của luồng khởi chạy. is_alive(): Trả vềTrue, nếu luồng đang chạy.-
run(): Phương thức chứa mã, sẽ được thực thi trong luồng. Ghi đè khi kế thừa từ lớpThread.
Mọi thứ đơn giản hơn nhiều so với vẻ ngoài — ví dụ sử dụng lớp Thread.
Khởi động một luồng đơn giản
import threading
def worker():
print("Worker thread is running")
# Tạo một luồng mới
t = threading.Thread(target=worker) #tạo đối tượng luồng mới
t.start() #Khởi động luồng
t.join() # Đợi hoàn thành của luồng
print("Main thread is finished")
Sau khi gọi phương thức start, hàm worker sẽ bắt đầu thực thi. Hoặc, chính xác hơn, luồng của nó sẽ được thêm vào danh sách các luồng hoạt động.
Sử dụng đối số
import threading
def worker(number, text):
print(f"Worker {number}: {text}")
# Tạo một luồng mới với đối số
t = threading.Thread(target=worker, args=(1, "Hello"))
t.start()
t.join()
Để chuyển tham số vào luồng mới, chỉ cần chỉ ra chúng dưới dạng tuple và gán cho tham số args. Khi gọi hàm đã được chỉ định trong target, các tham số sẽ được chuyển một cách tự động.
Ghi đè phương thức run
import threading
class MyThread(threading.Thread):
def run(self):
print("Custom thread is running")
# Tạo và khởi động luồng
t = MyThread()
t.start()
t.join()
Có hai cách để xác định hàm bắt đầu thực thi luồng mới — có thể truyền nó qua tham số target khi tạo đối tượng Thread, hoặc kế thừa từ lớp Thread và ghi đè hàm run. Cả hai cách đều hợp lệ và thường được sử dụng.
2.3 Lớp Timer
Lớp Timer trong mô-đun threading được dùng để khởi động một hàm sau một khoảng thời gian xác định. Lớp này hữu ích cho việc thực hiện các nhiệm vụ trì hoãn trong môi trường đa luồng.
Timer được tạo và khởi tạo với hàm cần gọi và thời gian trì hoãn tính bằng giây.
- Phương thức
start()khởi động timer, đếm ngược khoảng thời gian xác định, sau đó gọi hàm chỉ định. - Phương thức
cancel()cho phép dừng đồng hồ nếu nó chưa sập nổ. Điều này hữu ích để ngăn chặn thực hiện hàm nếu timer không còn cần nữa.
Ví dụ sử dụng:
Khởi động hàm với độ trễ
Trong ví dụ này, hàm hello sẽ được gọi sau 5 giây kể từ khi khởi động timer.
import threading
def hello():
print("Hello, world!")
# Tạo timer, sẽ gọi hàm hello sau 5 giây
t = threading.Timer(5.0, hello)
t.start() # Bắt đầu timer
Ngừng timer trước khi thực thi
Ở đây timer sẽ bị ngừng trước khi hàm hello kịp thực thi, vì thế không có gì được in ra.
import threading
def hello():
print("Hello, world!")
# Tạo timer
t = threading.Timer(5.0, hello)
t.start() # Bắt đầu timer
# Ngừng timer trước khi thực thi
t.cancel()
Timer với tham số
Trong ví dụ này, timer sẽ gọi hàm greet sau 3 giây và chuyển cho nó tham số "Alice".
import threading
def greet(name):
print(f"Hello, {name}!")
# Tạo timer với tham số
t = threading.Timer(3.0, greet, args=["Alice"])
t.start()
Lớp Timer tiện lợi cho việc lập kế hoạch thực hiện các nhiệm vụ trong một khoảng thời gian nhất định. Tuy nhiên, timer không đảm bảo thời gian thực thi chính xác tuyệt đối, vì điều này phụ thuộc vào tải của hệ thống và hoạt động của bộ lập lịch luồng.
2.4 Lớp ThreadLocal
Lớp ThreadLocal được dùng để tạo luồng mà mỗi luồng có dữ liệu cục bộ riêng của nó. Điều này hữu ích trong các ứng dụng đa luồng khi mỗi luồng cần có phiên bản dữ liệu riêng để tránh xung đột và vấn đề đồng bộ hóa.
Mỗi luồng sử dụng ThreadLocal sẽ có các bản sao dữ liệu độc lập. Dữ liệu được lưu trữ trong đối tượng ThreadLocal là duy nhất cho từng luồng và không chia sẻ với các luồng khác. Điều này tiện lợi để lưu trữ dữ liệu chỉ sử dụng trong ngữ cảnh của một luồng duy nhất như người dùng hiện tại trong ứng dụng web hoặc kết nối hiện tại với cơ sở dữ liệu.
Ví dụ sử dụng:
Sử dụng cơ bản
Trong ví dụ này, mỗi luồng gán tên của chính nó cho biến cục bộ value và in ra. Giá trị value là duy nhất cho mỗi luồng.
import threading
# Tạo đối tượng ThreadLocal
local_data = threading.local()
def process_data():
# Gán giá trị cho biến cục bộ của luồng
local_data.value = threading.current_thread().name
# Truy cập biến cục bộ của luồng
print(f'Value in {threading.current_thread().name}: {local_data.value}')
threads = []
for i in range(5):
t = threading.Thread(target=process_data)
threads.append(t)
t.start()
for t in threads:
t.join()
Lưu trữ dữ liệu người dùng trong ứng dụng web
Trong ví dụ này, mỗi luồng xử lý yêu cầu cho người dùng của riêng nó. Giá trị user_data.user là duy nhất cho mỗi luồng.
import threading
# Tạo đối tượng ThreadLocal
user_data = threading.local()
def process_request(user):
# Gán giá trị cho biến cục bộ của luồng
user_data.user = user
handle_request()
def handle_request():
# Truy cập biến cục bộ của luồng
print(f'Handling request for user: {user_data.user}')
threads = []
users = ['Alice', 'Bob', 'Charlie']
for user in users:
t = threading.Thread(target=process_request, args=(user,))
threads.append(t)
t.start()
for t in threads:
t.join()
Đó là 3 lớp hữu ích nhất của thư viện threading. Có lẽ bạn sẽ sử dụng chúng trong công việc của mình, còn các lớp khác — chắc là không cần lắm. Giờ mọi người đều chuyển sang sử dụng chức năng bất đồng bộ và thư viện asyncio. Chúng ta sẽ nói về nó trong thời gian sắp tới.
GO TO FULL VERSION