Đảm bảo an toàn chủ đề

Trang này ghi lại các đảm bảo an toàn luồng cho các loại tích hợp trong bản dựng luồng tự do của Python. Các đảm bảo được mô tả ở đây áp dụng khi sử dụng Python khi tắt GIL (chế độ luồng tự do). Khi GIL được bật, hầu hết các hoạt động đều được tuần tự hóa ngầm.

Để biết hướng dẫn chung về cách viết mã an toàn luồng bằng Python có luồng tự do, hãy xem Hỗ trợ Python cho luồng miễn phí.

Mức độ an toàn của chủ đề

Tài liệu C API sử dụng các cấp độ sau để mô tả đảm bảo an toàn luồng của từng chức năng. Các cấp độ được liệt kê từ ít nhất đến an toàn nhất.

Không tương thích

Một chức năng hoặc hoạt động không thể đảm bảo an toàn khi sử dụng đồng thời ngay cả khi đồng bộ hóa bên ngoài. Mã không tương thích thường truy cập trạng thái chung theo cách không đồng bộ và chỉ được gọi từ một luồng duy nhất trong suốt vòng đời của chương trình.

Ví dụ: một hàm sửa đổi trạng thái toàn quy trình, chẳng hạn như trình xử lý tín hiệu hoặc biến môi trường, trong đó các lệnh gọi đồng thời từ bất kỳ luồng nào, ngay cả với khóa bên ngoài, có thể xung đột với thời gian chạy hoặc các thư viện khác.

Tương thích

Một chức năng hoặc thao tác an toàn để gọi từ nhiều luồng provided, người gọi sẽ cung cấp khả năng đồng bộ hóa bên ngoài thích hợp, chẳng hạn như bằng cách giữ lock trong suốt thời gian của mỗi cuộc gọi. Nếu không đồng bộ hóa như vậy, các cuộc gọi đồng thời có thể tạo ra race conditions hoặc data races.

Ví dụ: một hàm đọc hoặc ghi vào một đối tượng có trạng thái bên trong không được bảo vệ bằng khóa. Người gọi phải đảm bảo rằng không có hai luồng nào truy cập vào cùng một đối tượng cùng một lúc.

An toàn trên các vật thể riêng biệt

Một hàm hoặc thao tác an toàn để gọi từ nhiều luồng mà không cần đồng bộ hóa bên ngoài, miễn là mỗi luồng hoạt động trên một đối tượng different. Hai luồng có thể gọi hàm cùng lúc, nhưng chúng không được truyền cùng một đối tượng (hoặc các đối tượng có chung trạng thái cơ bản) làm đối số.

Ví dụ: một hàm sửa đổi các trường của cấu trúc bằng cách ghi phi nguyên tử. Mỗi luồng có thể gọi hàm trên phiên bản cấu trúc riêng của chúng một cách an toàn, nhưng các lệnh gọi đồng thời trên phiên bản same yêu cầu đồng bộ hóa bên ngoài.

An toàn trên các đối tượng được chia sẻ

Một chức năng hoặc thao tác an toàn để sử dụng đồng thời trên đối tượng same. Việc triển khai sử dụng đồng bộ hóa nội bộ (chẳng hạn như per-object locks hoặc critical sections) để bảo vệ trạng thái có thể thay đổi được chia sẻ, do đó người gọi không cần cung cấp khóa riêng của họ.

Ví dụ: PyList_GetItemRef() có thể được gọi từ nhiều luồng trên cùng một PyListObject - nó sử dụng đồng bộ hóa nội bộ để tuần tự hóa quyền truy cập.

nguyên tử

Một hàm hoặc thao tác xuất hiện atomic đối với các luồng khác - nó thực thi ngay lập tức từ góc nhìn của các luồng khác. Đây là hình thức an toàn chủ đề mạnh nhất.

Ví dụ: PyMutex_IsLocked() thực hiện đọc nguyên tử trạng thái mutex và có thể được gọi từ bất kỳ luồng nào vào bất kỳ lúc nào.

An toàn chủ đề cho các đối tượng danh sách

Đọc một phần tử từ listatomic:

lst[i] # list.__getitem__

Các phương pháp sau đây duyệt qua danh sách và sử dụng số lần đọc atomic của từng mục để thực hiện chức năng của chúng. Điều đó có nghĩa là chúng có thể trả về kết quả bị ảnh hưởng bởi các sửa đổi đồng thời:

mục trong lst
lst.index(mục)
lst.count(item)

Tất cả các thao tác trên đều tránh việc thu được per-object locks. Chúng không chặn các sửa đổi đồng thời. Các hoạt động khác có khóa sẽ không chặn các hoạt động này quan sát các trạng thái trung gian.

Tất cả các hoạt động khác từ đây đều được chặn bằng cách sử dụng per-object lock.

Viết một mục duy nhất qua lst[i] = x là an toàn để gọi từ nhiều luồng và sẽ không làm hỏng danh sách.

Các thao tác sau trả về các đối tượng mới và xuất hiện atomic cho các luồng khác:

lst1 + lst2 # concatenates hai danh sách thành một danh sách mới
x * lst # repeats lst x lần vào danh sách mới
lst.copy() # returns một bản sao nông của danh sách

Các phương thức sau chỉ hoạt động trên một phần tử duy nhất mà không cần dịch chuyển là atomic:

lst.append(x) # append vào cuối danh sách, không cần dịch chuyển
Phần tử lst.pop() # pop ở cuối danh sách, không cần dịch chuyển

Phương thức clear() cũng là atomic. Các chủ đề khác không thể quan sát các phần tử bị xóa.

Phương thức sort() không phải là atomic. Các luồng khác không thể quan sát các trạng thái trung gian trong quá trình sắp xếp, nhưng danh sách sẽ trống trong suốt thời gian sắp xếp.

Các hoạt động sau đây có thể cho phép các hoạt động lock-free quan sát các trạng thái trung gian vì chúng sửa đổi nhiều thành phần tại chỗ:

lst.insert(idx, item) phần tử # shifts
lst.pop(idx) # idx không ở cuối danh sách, dịch chuyển các phần tử
lst *= x # copies phần tử được đặt đúng chỗ

Phương thức remove() có thể cho phép sửa đổi đồng thời vì so sánh phần tử có thể thực thi mã Python tùy ý (thông qua __eq__()).

extend() có thể gọi từ nhiều luồng một cách an toàn. Tuy nhiên, sự đảm bảo của nó phụ thuộc vào khả năng lặp được truyền cho nó. Nếu đó là list, tuple, set, frozenset, dict hoặc dictionary view object (nhưng không phải là lớp con của chúng), hoạt động extend sẽ an toàn trước các sửa đổi đồng thời đối với iterable. Mặt khác, một trình vòng lặp được tạo ra có thể được sửa đổi đồng thời bởi một luồng khác. Điều tương tự cũng áp dụng cho việc ghép nối tại chỗ một danh sách với các lần lặp khác khi sử dụng lst += iterable.

Tương tự, việc gán cho một lát danh sách với lst[i:j] = iterable là an toàn khi gọi từ nhiều luồng, nhưng iterable chỉ bị khóa khi nó cũng là list (chứ không phải các lớp con của nó).

Các hoạt động liên quan đến nhiều quyền truy cập cũng như sự lặp lại không bao giờ mang tính nguyên tử. Ví dụ:

# NOT nguyên tử: đọc-sửa-ghi
lst[i] = lst[i] + 1

# NOT nguyên tử: kiểm tra rồi hành động
nếu lst:
    mục = lst.pop()

# NOT thread-safe: lặp lại trong khi sửa đổi
cho mục trong lst:
    tiến trình(item) chủ đề # another có thể sửa đổi lst

Xem xét đồng bộ hóa bên ngoài khi chia sẻ phiên bản list trên các luồng.

An toàn chủ đề cho các đối tượng dict

Việc tạo một từ điển bằng hàm tạo dict là đơn giản khi đối số của nó là dict hoặc tuple. Khi sử dụng phương pháp dict.fromkeys(), việc tạo từ điển là nguyên tử khi đối số là dict, tuple, set hoặc frozenset.

Các hoạt động và chức năng sau đây là lock-freeatomic.

d[key] # dict.__getitem__
d.get(key) # dict.get
nhập vào d # dict.__contains__
len(d) # dict.__len__

Tất cả các hoạt động khác từ đây sẽ được giữ per-object lock.

Việc viết hoặc xóa một mục sẽ an toàn khi gọi từ nhiều luồng và sẽ không làm hỏng từ điển:

d[key] = giá trị # write
del d[key] # delete
d.pop(key) # remove và quay lại
d.popitem() # remove và trả về mục cuối cùng
d.setdefault(key, v) # insert nếu thiếu

Các thao tác này có thể so sánh các khóa bằng __eq__(), có thể thực thi mã Python tùy ý. Trong quá trình so sánh như vậy, từ điển có thể được sửa đổi bởi một luồng khác. Đối với các loại tích hợp như str, intfloat, triển khai __eq__() trong C, khóa cơ bản không được giải phóng trong quá trình so sánh và đây không phải là vấn đề đáng lo ngại.

Các thao tác sau trả về các đối tượng mới và giữ per-object lock trong suốt thời gian thực hiện thao tác:

d.copy() # returns một bản sao nông của từ điển
d | # merges khác hai lệnh thành một lệnh mới
d.keys() # returns một đối tượng xem dict_keys mới
d.values() # returns một đối tượng xem dict_values mới
d.items() # returns một đối tượng xem dict_items mới

Phương thức clear() giữ khóa trong suốt thời gian của nó. Các chủ đề khác không thể quan sát các phần tử bị xóa.

Các thao tác sau sẽ khóa cả hai từ điển. Đối với update()|=, điều này chỉ áp dụng khi toán hạng khác là dict sử dụng trình lặp chính tả tiêu chuẩn (nhưng không áp dụng cho các lớp con ghi đè phép lặp). Để so sánh bình đẳng, điều này áp dụng cho dict và các lớp con của nó:

d.update(other_dict) # both bị khóa khi other_dict là một lệnh
d |= other_dict # both bị khóa khi other_dict là một dict
d == other_dict # both bị khóa đối với dict và các lớp con

Tất cả các hoạt động so sánh cũng so sánh các giá trị bằng cách sử dụng __eq__(), do đó, đối với các loại không tích hợp sẵn, khóa có thể được giải phóng trong quá trình so sánh.

fromkeys() khóa cả từ điển mới và iterable khi iterable chính xác là dict, set hoặc frozenset (không phải lớp con):

dict.fromkeys(a_dict) # locks cả hai
dict.fromkeys(a_set) # locks cả hai
dict.fromkeys(a_frozenset) # locks cả hai

Khi cập nhật từ một lần lặp không chính tả, chỉ có từ điển đích bị khóa. Việc lặp lại có thể được sửa đổi đồng thời bởi một luồng khác:

d.update(iterable) # iterable không phải là một lệnh: chỉ có d bị khóa
d |= iterable # iterable không phải là một lệnh: chỉ có d bị khóa
dict.fromkeys(iterable) # iterable không phải là dict/set/frozenset: chỉ khóa kết quả

Các hoạt động liên quan đến nhiều quyền truy cập cũng như việc lặp lại không bao giờ mang tính nguyên tử:

# NOT nguyên tử: đọc-sửa-ghi
d[khóa] = d[khóa] + 1

# NOT nguyên tử: kiểm tra rồi hành động (TOCTOU)
nếu nhập vào d:
    del d[khóa]

# NOT thread-safe: lặp lại trong khi sửa đổi
đối với khóa, giá trị trong d.items():
    tiến trình (khóa) luồng # another có thể sửa đổi d

Để tránh các vấn đề về thời gian kiểm tra đối với thời gian sử dụng (TOCTOU), hãy sử dụng các thao tác nguyên tử hoặc xử lý các ngoại lệ:

# Use pop() với mặc định thay vì kiểm tra rồi xóa
d.pop(khóa, Không )

# Or xử lý ngoại lệ
thử:
    del d[khóa]
ngoại trừ KeyError:
    vượt qua

Để lặp lại một cách an toàn một từ điển có thể được sửa đổi bởi một luồng khác, hãy lặp lại một bản sao:

# Make một bản sao để lặp lại một cách an toàn
đối với khóa, giá trị trong d.copy().items():
    quá trình (khóa)

Xem xét đồng bộ hóa bên ngoài khi chia sẻ phiên bản dict trên các luồng.

An toàn luồng cho các đối tượng được thiết lập

Chức năng len() không khóa và atomic.

Thao tác đọc sau đây không bị khóa. Nó không chặn các sửa đổi đồng thời và có thể quan sát các trạng thái trung gian từ các hoạt động giữ khóa cho mỗi đối tượng:

phần tử trong s # set.__contains__

Thao tác này có thể so sánh các phần tử bằng __eq__(), có thể thực thi mã Python tùy ý. Trong quá trình so sánh như vậy, tập hợp có thể được sửa đổi bởi một luồng khác. Đối với các loại tích hợp như str, intfloat, __eq__() không giải phóng khóa cơ bản trong quá trình so sánh và đây không phải là vấn đề đáng lo ngại.

Tất cả các hoạt động khác từ đây trở đi đều giữ khóa theo đối tượng.

Việc thêm hoặc xóa một phần tử là an toàn để gọi từ nhiều luồng và sẽ không làm hỏng tập hợp:

phần tử s.add(elem) # add
Phần tử s.remove(elem) # remove, tăng nếu thiếu
phần tử s.discard(elem) # remove nếu có
s.pop() # remove và trả về phần tử tùy ý

Các thao tác này cũng so sánh các phần tử, do đó, các cân nhắc __eq__() tương tự như trên sẽ được áp dụng.

Phương thức copy() trả về một đối tượng mới và giữ khóa cho mỗi đối tượng trong suốt thời gian để nó luôn ở dạng nguyên tử.

Phương thức clear() giữ khóa trong suốt thời gian của nó. Các chủ đề khác không thể quan sát các phần tử bị xóa.

Các thao tác sau chỉ chấp nhận set hoặc frozenset làm toán hạng và luôn khóa cả hai đối tượng:

s |= # other khác phải được đặt/đóng băng
s &= # other khác phải được đặt/đóng băng
s -= # other khác phải được đặt/đóng băng
s ^= # other khác phải được đặt/đóng băng
s & # other khác phải được đặt/đóng băng
s | # other khác phải được đặt/đóng băng
s - # other khác phải được đặt/đóng băng
s ^ # other khác phải được đặt/đóng băng

set.update(), set.union(), set.intersection()set.difference() có thể lấy nhiều lần lặp làm đối số. Tất cả đều lặp qua tất cả các lần lặp đã qua và thực hiện như sau:

set.symmetric_difference() cố gắng khóa cả hai đối tượng.

Các biến thể cập nhật của các phương pháp trên cũng có một số khác biệt giữa chúng:

Các phương pháp sau đây luôn cố gắng khóa cả hai đối tượng:

s.isdisjoint(other) # both đã bị khóa
s.issubset(other) # both đã bị khóa
s.issuperset(other) # both đã bị khóa

Các hoạt động liên quan đến nhiều quyền truy cập cũng như việc lặp lại không bao giờ mang tính nguyên tử:

# NOT nguyên tử: kiểm tra rồi hành động
nếu phần tử trong s:
      s.remove(elem)

# NOT thread-safe: lặp lại trong khi sửa đổi
cho phần tử trong s:
      thread(elem) # another thread có thể sửa đổi s

Xem xét đồng bộ hóa bên ngoài khi chia sẻ phiên bản set trên các luồng. Xem Hỗ trợ Python cho luồng miễn phí để biết thêm thông tin.

An toàn luồng cho các đối tượng bytearray

Chức năng len() không khóa và atomic.

Ghép nối và so sánh sử dụng giao thức đệm, ngăn chặn việc thay đổi kích thước nhưng không giữ khóa cho mỗi đối tượng. Các hoạt động này có thể quan sát các trạng thái trung gian từ các sửa đổi đồng thời:

ba + # may khác quan sát ghi đồng thời
ba == # may khác quan sát ghi đồng thời
ba < # may khác quan sát ghi đồng thời

Tất cả các hoạt động khác từ đây trở đi đều giữ khóa theo đối tượng.

Đọc một phần tử hoặc lát cắt là an toàn để gọi từ nhiều luồng:

ba[i] # bytearray.__getitem__
ba[i:j] # slice

Các thao tác sau đây có thể gọi an toàn từ nhiều luồng và sẽ không làm hỏng bytearray:

ba[i] = x # write byte đơn
ba[i:j] = giá trị lát cắt # write
ba.append(x) # append byte đơn
ba.extend(other) # extend với khả năng lặp lại
ba.insert(i, x) # insert byte đơn
ba.pop() # remove và trả về byte cuối cùng
ba.pop(i) # remove và trả về byte tại chỉ mục
ba.remove(x) # remove xuất hiện lần đầu
ba.reverse() # reverse tại chỗ
ba.clear() # remove tất cả byte

Phép gán lát khóa cả hai đối tượng khi valuesbytearray:

ba[i:j] = other_bytearray # both đã bị khóa

Các thao tác sau trả về các đối tượng mới và giữ khóa theo từng đối tượng trong suốt thời gian:

ba.copy() # returns một bản sao nông
ba * n # repeat vào bytearray mới

Bài kiểm tra tư cách thành viên giữ khóa trong suốt thời gian của nó:

x trong ba # bytearray.__contains__

Tất cả các phương thức bytearray khác (chẳng hạn như find(), replace(), split(), decode(), v.v.) đều giữ khóa mỗi đối tượng trong suốt thời gian của chúng.

Các hoạt động liên quan đến nhiều quyền truy cập cũng như việc lặp lại không bao giờ mang tính nguyên tử:

# NOT nguyên tử: kiểm tra rồi hành động
nếu x trong ba:
    ba.remove(x)

# NOT thread-safe: lặp lại trong khi sửa đổi
cho byte trong ba:
    luồng xử  (byte) # another có thể sửa đổi ba

Để lặp lại một cách an toàn trên một bytearray có thể được sửa đổi bởi một luồng khác, hãy lặp qua một bản sao:

# Make một bản sao để lặp lại một cách an toàn
cho byte trong ba.copy():
    quá trình (byte)

Xem xét đồng bộ hóa bên ngoài khi chia sẻ phiên bản bytearray trên các luồng. Xem Hỗ trợ Python cho luồng miễn phí để biết thêm thông tin.

An toàn luồng cho các đối tượng MemoryView

Đối tượng memoryview cung cấp quyền truy cập vào dữ liệu nội bộ của đối tượng cơ bản mà không cần sao chép. Sự an toàn của luồng phụ thuộc vào cả bản thân bộ nhớ và trình xuất bộ đệm cơ bản.

Việc triển khai chế độ xem bộ nhớ sử dụng các hoạt động nguyên tử để theo dõi quá trình xuất của chính nó trong free-threaded build. Việc tạo và giải phóng chế độ xem bộ nhớ là an toàn theo luồng. Quyền truy cập thuộc tính (ví dụ: shape, format) đọc các trường không thay đổi trong suốt thời gian tồn tại của chế độ xem bộ nhớ, do đó, việc đọc đồng thời sẽ an toàn miễn là chế độ xem bộ nhớ chưa được giải phóng.

Tuy nhiên, dữ liệu thực tế được truy cập thông qua chế độ xem bộ nhớ thuộc sở hữu của đối tượng cơ bản. Quyền truy cập đồng thời vào dữ liệu này chỉ an toàn nếu đối tượng cơ bản hỗ trợ nó:

  • Đối với các đối tượng bất biến như bytes, việc đọc đồng thời qua nhiều chế độ xem bộ nhớ là an toàn.

  • Đối với các đối tượng có thể thay đổi như bytearray, việc đọc và ghi cùng một vùng bộ nhớ từ nhiều luồng mà không đồng bộ hóa bên ngoài là không an toàn và có thể dẫn đến hỏng dữ liệu. Lưu ý rằng ngay cả chế độ xem bộ nhớ chỉ đọc của các đối tượng có thể thay đổi cũng không ngăn cản được việc chạy đua dữ liệu nếu đối tượng cơ bản được sửa đổi từ một luồng khác.

# NOT an toàn: ghi đồng thời vào cùng một bộ đệm
dữ liệu = bytearray(1000)
xem = bộ nhớ xem (dữ liệu)
# Thread 1: lượt xem[0:500] = b'x' * 500
# Thread 2: view[0:500] = b'y' * 500
# Safe: sử dụng khóa để truy cập đồng thời
nhập luồng
lock = threading.Lock()
dữ liệu = bytearray(1000)
xem = bộ nhớ xem (dữ liệu)

 khóa:
    lượt xem[0:500] = b'x' * 500

Việc thay đổi kích thước hoặc phân bổ lại đối tượng cơ bản (chẳng hạn như gọi bytearray.resize()) trong khi xuất chế độ xem bộ nhớ sẽ làm tăng BufferError. Điều này được thực thi bất kể luồng.