Đa Luồng Trong Python: Đồng Bộ Hóa Và Giải Quyết Deadlock

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:

  1. Luồng gọi acquire() sẽ chiếm giữ khóa
  2. Các luồng khác gọi acquire() sẽ bị chặn cho đến khi khóa được giải phóng
  3. 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

Thẻ: Python threading Mutex lock Deadlock prevention UDP socket programming Concurrency control

Đăng vào ngày 19 tháng 5 lúc 15:15