Khái Niệm Đa Nhiệm Trong Hệ Thống
Trong thực tế, nhiều hoạt động diễn ra đồng thời như vừa lái xe vừa điều khiển vô lăng, hoặc vừa hát vừa nhảy múa. Nếu thực hiện tuần tự (hát xong mới nhảy), hiệu quả sẽ giảm đáng kể. Tương tự trong lập trình, để xử lý song song các tác vụ, chúng ta cần cơ chế đa luồng.
Phân Biệt Đồng Thời Và Song Song
- Đồng thời (Concurrency): Số tác vụ vượt quá số lõi CPU, hệ điều hành luân phiên thực thi qua cơ chế scheduling. Nhờ tốc độ chuyển đổi nhanh, các tác vụ dường như chạy cùng lúc.
- Song song (Parallelism): Số tác vụ ≤ số lõi CPU, các tác vụ thực thi thực sự đồng thời trên các lõi riêng biệt.
Cơ Chế Đa Luồng Với Threading
Thư viện threading cung cấp giao diện cao cấp hơn so với module nền thread. Ví dụ minh họa:
import threading
import time
def task_a():
for _ in range(3):
print("Thực hiện tác vụ A")
time.sleep(0.5)
def task_b():
for _ in range(3):
print("Thực hiện tác vụ B")
time.sleep(0.5)
# Khởi tạo luồng
thread_a = threading.Thread(target=task_a)
thread_b = threading.Thread(target=task_b)
# Bắt đầu thực thi
thread_a.start()
thread_b.start()
Chương trình trên cho thấy hai tác vụ chạy xen kẽ thay vì tuần tự. Lưu ý: Luồng chính sẽ chờ tất cả luồng con kết thúc trước khi thoát.
Chia Sẻ Biến Toàn Cục Và Vấn Đề
Các luồng trong cùng tiến trình chia sẻ không gian bộ nhớ, dẫn đến rủi ro khi thao tác trên biến toàn cục:
shared_counter = 0
def increment():
global shared_counter
for _ in range(1000000):
shared_counter += 1
thread_x = threading.Thread(target=increment)
thread_y = threading.Thread(target=increment)
thread_x.start()
thread_y.start()
thread_x.join()
thread_y.join()
print(f"Kết quả cuối: {shared_counter}") # Thường nhỏ hơn 2000000
Lỗi xảy ra do các luồng đọc/ghi chồng chéo lên nhau khi cập nhật biến chia sẻ. Khi một luồng đọc giá trị, hệ thống tạm dừng nó trước khi ghi, luồng khác sửa đổi giá trị khiến kết quả không chính xác.
Cơ Chế Đồng Bộ Hóa Với Khóa Độc Quyền
Để giải quyết xung đột tài nguyên, sử dụng Lock từ thư viện threading:
from threading import Lock
counter = 0
lock = Lock()
def safe_increment():
global counter
for _ in range(1000000):
lock.acquire() # Đặt khóa
counter += 1
lock.release() # Tháo khóa
thread_1 = threading.Thread(target=safe_increment)
thread_2 = threading.Thread(target=safe_increment)
thread_1.start()
thread_2.start()
thread_1.join()
thread_2.join()
print(f"Giá trị chính xác: {counter}") # Luôn bằng 2000000
Quy trình hoạt động:
- Luồng gọi
acquire()sẽ chiếm giữ khóa - Các luồng khác gọi
acquire()sẽ bị chặn cho đến khi khóa được giải phóng - Chỉ một luồng được thực thi đoạn code được bảo vệ tại bất kỳ thời điểm nào
Deadlock Và Cách Phòng Tránh
Deadlock xảy ra khi hai hoặc nhiều luồng giữ khóa và chờ lẫn nhau giải phóng tài nguyên:
lock_x = Lock()
lock_y = Lock()
def thread_one():
lock_x.acquire()
time.sleep(0.1)
lock_y.acquire() # Chờ lock_y từ thread_two
# ... xử lý
lock_y.release()
lock_x.release()
def thread_two():
lock_y.acquire()
time.sleep(0.1)
lock_x.acquire() # Chờ lock_x từ thread_one
# ... xử lý
lock_x.release()
lock_y.release()
Các biện pháp phòng tránh:
- Sắp xếp thứ tự khóa nhất quán trên tất cả luồng
- Sử dụng timeout khi yêu cầu khóa:
lock.acquire(timeout=2.0) - Áp dụng thuật toán Banker để phân bổ tài nguyên an toàn
Ứng Dụng Thực Tế: Chat UDP Đa Luồng
Triển khai ứng dụng chat sử dụng hai luồng riêng biệt:
import socket
import threading
def receive_data(sock):
while True:
data, addr = sock.recvfrom(1024)
print(f"\n[NHẬN] Từ {addr}: {data.decode()}")
def send_data(sock):
while True:
message = input("[GỬI] Nhập tin nhắn: ")
target_ip = input("IP đích: ")
target_port = int(input("Cổng: "))
sock.sendto(message.encode(), (target_ip, target_port))
def main():
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.bind(('', 9999))
receiver = threading.Thread(target=receive_data, args=(udp_socket,))
receiver.daemon = True
receiver.start()
send_data(udp_socket)
if __name__ == "__main__":
main()
Đặc điểm chính của ứng dụng đa luồng:
- Luồng nhận dữ liệu chạy nền (daemon thread)
- Luồng chính xử lý giao tiếp người dùng
- Cả hai luồng truy cập chung tài nguyên socket nhưng không xung đột nhờ cơ chế blocking I/O