Race condition là gì? Cách Python ngăn chặn race condition |
Race condition là gì?
Race condition (tình trạng tranh chấp) là một trong những tình trạng không mong muốn xảy ra khi một thiết bị hoặc một hệ thống cố gắng thực hiện hai hay nhiều thao tác cùng một lúc, nhưng do bản chất của thiết bị hoặc hệ thống, các thao tác này phải thực hiện đúng theo trình tự mới có thể hoàn thành chính xác.
Race condition thường được nhắc đến nhiều nhất trong khoa học máy tính và lập trình, chúng thường xảy ra khi hai tiến trình của chương trình máy tính, hoặc hai luông (thread) cố gắng truy cập cùng một tài nguyên tại cùng một thời điểm và gây ra các vấn đề trong hệ thống.
Race condition được coi là một vấn đề phổ biến đối với các ứng dụng đa nguồn. Trong bài viết này, chúng ta sẽ tìm hiểu về race condition trong ngôn ngữ lập trình Python.
Ví dụ về Race Condition trong Reference Counting
Giả sử chúng ta có một đối tượng shared_list được tham chiếu hai biến
shared_list = [1, 2, 3] # ref_count = 1
another_reference = shared_list # ref_count = 2
Cách Python quản lý Reference Counting trong trường hợp lý tưởng
# Giả lập cách Python quản lý reference count nội bộ
class ObjectWithCount:
def __init__(self, value):
self.value = value
self.ref_count = 0
def increase_ref(self):
self.ref_count += 1
def decrease_ref(self):
self.ref_count -= 1
if self.ref_count == 0:
# Giải phóng bộ nhớ
del self.value
# Sử dụng bình thường (single-thread)
obj = ObjectWithCount([1, 2, 3]) # ref_count = 0
obj.increase_ref() # ref_count = 1
reference2 = obj # ref_count = 2
obj.increase_ref()
Race Condition khi không có GIL
Giả sử có 2 thread cùng thực hiện việc giảm reference count:
# Thread 1 # Thread 2
read ref_count (value = 2)
read ref_count (value = 2)
decrease to 1
decrease to 1
write back ref_count = 1
write back ref_count = 1
Điểu gì sẽ xảy ra?
- Ban đầu, ref_count = 2
- Cả hai thread đều đọc ref_count = 2
- Mỗi thread giảm giá trị xuống 1
- Cả hai thread đều ghi lại giá trị 1
- Kết quả cuối cùng ref_count = 1 (sai), trong khi đáng lẽ phải bằng 0
Hệ quả nghiêm trọng
Memory Leak: Đối tượng sẽ không bao giờ được giải phóng vì ref_count không bao giờ bằng 0, bộ nhớ bị chiếm mãi mãi
Segmentation Fault: Ngược lại, nếu ref_count = 0 khi vẫn còn tham chiếu, đối tượng được giải phóng trong khi vấn đang được sử dụng, khiến cho trường trình có thể bị crash
Ví dụ thực tế về Memory Leak
import threading
class SharedResource:
def __init__(self, data):
self._data = data
self.ref_count = 0
def add_reference(self):
# Giả sử không có GIL
current = self.ref_count # Thread 1 đọc
# Context switch - Thread 2 chạy
# Thread 2 cũng đọc cùng giá trị
self.ref_count = current + 1 # Cả 2 thread cùng tăng
# Kết quả: tăng 1 lần thay vì 2 lần
def remove_reference(self):
# Tương tự với giảm reference
current = self.ref_count
# Context switch có thể xảy ra ở đây
self.ref_count = current - 1
if self.ref_count == 0:
del self._data
# Trong thực tế:
resource = SharedResource([1, 2, 3]) # ref_count = 0
def thread_function():
resource.add_reference()
# Sử dụng resource
resource.remove_reference()
# Tạo nhiều thread cùng sử dụng resource
threads = [threading.Thread(target=thread_function) for _ in range(5)]
# Chạy các thread
for t in threads:
t.start()
# Đợi các thread hoàn thành
for t in threads:
t.join()
# Có thể ref_count ≠ 0 dù không còn thread nào sử dụng
# => Memory leak
Giải pháp với GIL
# Với GIL, các operation trên ref_count được atomic
def add_reference():
# GIL đảm bảo chỉ một thread được thực thi
self.ref_count += 1 # An toàn, không có race condition
def remove_reference():
# GIL đảm bảo tính atomic
self.ref_count -= 1
if self.ref_count == 0:
del self._data # An toàn, không có memory leak
GIL đảm bảo chỉ một thread có thể thực thi Python bytecode tại một thời điểm, do đó:
- Không có race condition trong việc đọc, ghi ref_count
- Các operation trên ref_count là atomic
- Memory management hoạt động chính xác và an toàn