Thông báo: Download 4 khóa học Python từ cơ bản đến nâng cao tại đây.
Tìm hiểu về điều kiện race của threading Lock trong Python
Trong lập trình đa luồng, việc nhiều luồng cùng truy cập và thay đổi một biến chia sẻ có thể dẫn đến các kết quả không mong muốn và không thể đoán trước, gây ra hiện tượng gọi là "điều kiện race". Đây là một vấn đề phổ biến và tiềm ẩn nhiều nguy cơ trong các ứng dụng đòi hỏi tính đồng bộ và chính xác cao. Để giải quyết vấn đề này, Python cung cấp một công cụ hữu ích là threading.Lock
. Bài viết này sẽ giúp bạn hiểu rõ hơn về điều kiện race, cách sử dụng đối tượng Lock
để ngăn chặn chúng, và minh họa bằng các ví dụ cụ thể để bạn có thể áp dụng vào thực tế lập trình của mình.
Điều kiện race là gì?
Điều kiện race xảy ra khi hai hoặc nhiều luồng cùng truy cập một biến chia sẻ đồng thời, dẫn đến kết quả không thể đoán trước.
Trong tình huống này, luồng đầu tiên đọc giá trị từ biến chia sẻ. Cùng lúc đó, luồng thứ hai cũng đọc giá trị từ biến chia sẻ đó.
Sau đó, cả hai luồng cố gắng thay đổi giá trị của biến chia sẻ. Vì các cập nhật xảy ra đồng thời, nên sẽ tạo ra một cuộc "đua" để xác định thay đổi của luồng nào sẽ được lưu lại.
Bài viết này được đăng tại [free tuts .net]
Giá trị cuối cùng của biến chia sẻ phụ thuộc vào luồng nào hoàn thành việc cập nhật cuối cùng. Luồng nào thay đổi giá trị cuối cùng sẽ "thắng cuộc đua".
Ví dụ về điều kiện race
Ví dụ sau minh họa một điều kiện race:
from threading import Thread from time import sleep counter = 0 def increase(by): global counter local_counter = counter local_counter += by sleep(0.1) counter = local_counter print(f'counter={counter}') # tạo các luồng t1 = Thread(target=increase, args=(10,)) t2 = Thread(target=increase, args=(20,)) # bắt đầu các luồng t1.start() t2.start() # đợi các luồng hoàn thành t1.join() t2.join() print(f'Giá trị cuối cùng của counter là {counter}')
Trong chương trình này, cả hai luồng cố gắng thay đổi giá trị của biến counter
cùng lúc. Giá trị của biến counter
phụ thuộc vào luồng nào hoàn thành cuối cùng.
Nếu luồng t1
hoàn thành trước luồng t2
, bạn sẽ thấy kết quả sau:
counter=10 counter=20 Giá trị cuối cùng của counter là 20
Ngược lại, bạn sẽ thấy kết quả sau:
counter=20 counter=10 Giá trị cuối cùng của counter là 10
Sử dụng threading lock để ngăn chặn điều kiện race trong Python
Để ngăn chặn điều kiện race, bạn có thể sử dụng một khóa luồng (threading lock).
Khóa luồng là một nguyên thủy đồng bộ cung cấp quyền truy cập độc quyền vào tài nguyên chia sẻ trong một ứng dụng đa luồng. Khóa luồng cũng được gọi là mutex, viết tắt của "mutual exclusion" (loại trừ lẫn nhau).
Thông thường, một khóa luồng có hai trạng thái: khóa và mở khóa. Khi một luồng chiếm khóa, khóa chuyển sang trạng thái khóa. Luồng này có thể truy cập độc quyền vào tài nguyên chia sẻ.
Các luồng khác cố gắng chiếm khóa trong khi khóa đang bị khóa sẽ bị chặn và phải đợi cho đến khi khóa được giải phóng.
Trong Python, bạn có thể sử dụng lớp Lock
từ mô-đun threading
để tạo một đối tượng khóa:
Đầu tiên, tạo một thể hiện của lớp Lock
:
lock = Lock()
Mặc định, khóa được mở khóa cho đến khi một luồng chiếm nó.
Thứ hai, chiếm khóa bằng cách gọi phương thức acquire()
:
lock.acquire()
Thứ ba, giải phóng khóa khi luồng hoàn thành việc thay đổi biến chia sẻ:
lock.release()
Ví dụ sau cho thấy cách sử dụng đối tượng Lock
để ngăn chặn điều kiện race trong chương trình trước:
from threading import Thread, Lock from time import sleep counter = 0 def increase(by, lock): global counter lock.acquire() local_counter = counter local_counter += by sleep(0.1) counter = local_counter print(f'counter={counter}') lock.release() lock = Lock() # tạo các luồng t1 = Thread(target=increase, args=(10, lock)) t2 = Thread(target=increase, args=(20, lock)) # bắt đầu các luồng t1.start() t2.start() # đợi các luồng hoàn thành t1.join() t2.join() print(f'Giá trị cuối cùng của counter là {counter}')
Kết quả đầu ra:
counter=10 counter=30 Giá trị cuối cùng của counter là 30
Sử dụng khóa luồng với câu lệnh with
trong Python
Dễ dàng hơn để sử dụng đối tượng khóa với câu lệnh with
để chiếm và giải phóng khóa trong một khối mã:
import threading # Tạo đối tượng khóa lock = threading.Lock() # Thực hiện một số thao tác trong vùng quan trọng with lock: # Khóa đã được chiếm trong khối with # Thực hiện các thao tác trên tài nguyên chia sẻ # ... # khóa được giải phóng ngoài khối with
Ví dụ, bạn có thể sử dụng câu lệnh with
mà không cần gọi các phương thức acquire()
và release()
trong ví dụ trên như sau:
from threading import Thread, Lock from time import sleep counter = 0 def increase(by, lock): global counter with lock: local_counter = counter local_counter += by sleep(0.1) counter = local_counter print(f'counter={counter}') lock = Lock() # tạo các luồng t1 = Thread(target=increase, args=(10, lock)) t2 = Thread(target=increase, args=(20, lock)) # bắt đầu các luồng t1.start() t2.start() # đợi các luồng hoàn thành t1.join() t2.join() print(f'Giá trị cuối cùng của counter là {counter}')
Định nghĩa lớp Counter an toàn với luồng sử dụng đối tượng Lock trong Python
Ví dụ sau minh họa cách định nghĩa một lớp Counter an toàn với luồng sử dụng đối tượng Lock:
from threading import Thread, Lock from time import sleep class Counter: def __init__(self): self.value = 0 self.lock = Lock() def increase(self, by): with self.lock: current_value = self.value current_value += by sleep(0.1) self.value = current_value print(f'counter={self.value}') def main(): counter = Counter() # tạo các luồng t1 = Thread(target=counter.increase, args=(10,)) t2 = Thread(target=counter.increase, args=(20,)) # bắt đầu các luồng t1.start() t2.start() # đợi các luồng hoàn thành t1.join() t2.join() print(f'Giá trị cuối cùng của counter là {counter.value}') if __name__ == '__main__': main()
Kết bài
Điều kiện race là một vấn đề thường gặp khi nhiều luồng cùng truy cập và thay đổi một biến chia sẻ đồng thời. Để ngăn chặn điều này, việc sử dụng đối tượng khóa luồng (threading.Lock
) là rất cần thiết. Bằng cách sử dụng phương thức acquire()
để chiếm khóa và release()
để giải phóng khóa, ta có thể đảm bảo rằng chỉ có một luồng được truy cập và thay đổi biến chia sẻ tại một thời điểm. Hơn nữa, sử dụng đối tượng khóa với câu lệnh with
giúp việc quản lý khóa trở nên đơn giản và hiệu quả hơn. Nhờ đó, bạn có thể viết các chương trình đa luồng an toàn và chính xác, giảm thiểu rủi ro và tăng hiệu suất của ứng dụng.