Danh sách có an toàn không?


154

Tôi nhận thấy rằng nó thường được đề xuất sử dụng hàng đợi với nhiều luồng, thay vì danh sách và .pop(). Đây có phải là vì danh sách không an toàn cho chuỗi, hoặc vì một số lý do khác?


1
Thật khó để luôn biết chính xác những gì được đảm bảo an toàn cho luồng trong Python và thật khó để lý giải về an toàn của luồng trong nó. Ngay cả ví Bitcoin rất phổ biến Electrum cũng có lỗi đồng thời xuất phát từ việc này.
sudo

Câu trả lời:


181

Danh sách bản thân là chủ đề an toàn. Trong CPython, GIL bảo vệ chống lại truy cập đồng thời vào chúng và các triển khai khác cẩn thận sử dụng khóa hạt mịn hoặc kiểu dữ liệu được đồng bộ hóa để triển khai danh sách của chúng. Tuy nhiên, trong khi bản thân danh sách không thể bị hỏng do cố gắng truy cập đồng thời, dữ liệu của danh sách không được bảo vệ. Ví dụ:

L[0] += 1

không được đảm bảo để thực sự tăng L [0] nếu một luồng khác thực hiện điều tương tự, vì +=không phải là hoạt động nguyên tử. (Rất, rất ít thao tác trong Python thực sự là nguyên tử, vì hầu hết chúng có thể khiến mã Python tùy ý được gọi.) Bạn nên sử dụng Hàng đợi vì nếu bạn chỉ sử dụng danh sách không được bảo vệ, bạn có thể nhận hoặc xóa mục sai vì chủng tộc điều kiện.


1
Deque cũng an toàn chủ đề? Nó có vẻ thích hợp hơn cho việc sử dụng của tôi.
lemiant

20
Tất cả các đối tượng Python có cùng loại an toàn luồng - bản thân chúng không bị hỏng, nhưng dữ liệu của chúng có thể. bộ sưu tập.deque là những gì đằng sau các đối tượng Queue.Queue. Nếu bạn đang truy cập mọi thứ từ hai luồng, bạn thực sự nên sử dụng các đối tượng Queue.Queue. Có thật không.
Thomas Wouters

10
lemiant, deque là chủ đề an toàn. Từ Chương 2 của Fluent Python: "Class Collection.deque là hàng đợi hai đầu an toàn theo luồng được thiết kế để chèn và xóa nhanh từ cả hai đầu. [...] Các hoạt động nối và popleft là nguyên tử, do đó, deque an toàn với sử dụng như một hàng đợi LIFO trong các ứng dụng đa luồng mà không cần sử dụng khóa. "
Al Sweigart

3
Đây là câu trả lời về CPython hay về Python? Câu trả lời cho Python là gì?
dùng541686

@Nils: Uh, trang đầu tiên bạn liên quan đến nói Python thay vì CPython bởi vì nó được mô tả ngôn ngữ Python. Và liên kết thứ hai theo nghĩa đen nói rằng có nhiều triển khai ngôn ngữ Python, chỉ một liên kết phổ biến hơn. Với câu hỏi là về Python, câu trả lời sẽ mô tả những gì có thể được đảm bảo xảy ra trong bất kỳ triển khai phù hợp nào của Python, không chỉ riêng những gì xảy ra trong CPython nói riêng.
dùng541686

89

Để làm rõ một điểm trong câu trả lời xuất sắc của Thomas, cần đề cập rằng đó append() chủ đề an toàn.

Điều này là do không có lo ngại rằng dữ liệu được đọc sẽ ở cùng một nơi khi chúng ta viết thư cho nó. Các append()hoạt động không đọc dữ liệu, nó chỉ ghi dữ liệu vào danh sách.


1
PyList_Append đang đọc từ bộ nhớ. Bạn có nghĩa là việc đọc và ghi của nó xảy ra trong cùng một khóa GIL? github.com/python/cpython/blob/
Mạnh

1
@amwinter Có, toàn bộ cuộc gọi đến PyList_Appendđược thực hiện trong một khóa GIL. Nó được đưa ra một tham chiếu đến một đối tượng để chắp thêm. Nội dung của đối tượng đó có thể được thay đổi sau khi được đánh giá và trước khi cuộc gọi đến PyList_Appendđược thực hiện. Nhưng nó vẫn sẽ là cùng một đối tượng và được nối thêm một cách an toàn (nếu bạn làm như vậy lst.append(x); ok = lst[-1] is x, okdĩ nhiên có thể là Sai). Mã bạn tham chiếu không đọc từ đối tượng được nối thêm, ngoại trừ INCREF nó. Nó đọc và có thể phân bổ lại, danh sách được thêm vào.
greggo

3
Điểm của dotancohen là L[0] += xsẽ thực hiện __getitem__bật Lvà sau đó __setitem__bật L- nếu Lhỗ trợ, __iadd__nó sẽ thực hiện mọi thứ hơi khác ở giao diện đối tượng, nhưng vẫn có hai thao tác riêng biệt Lở cấp trình thông dịch python (bạn sẽ thấy chúng trong biên dịch mã byte). Việc appendnày được thực hiện trong một cuộc gọi phương thức đơn trong mã byte.
greggo

6
Thế còn remove?
acrazing

2
nâng cao tinh thần! vì vậy tôi có thể nối vào một chủ đề liên tục và bật trong một chủ đề khác không?
PirateApp


3

Gần đây tôi đã gặp trường hợp này khi tôi cần nối thêm một danh sách liên tục trong một luồng, lặp qua các mục và kiểm tra xem mục đó đã sẵn sàng chưa, đó là AsyncResult trong trường hợp của tôi và chỉ xóa nó khỏi danh sách nếu nó đã sẵn sàng. Tôi không thể tìm thấy bất kỳ ví dụ nào chứng minh rõ ràng vấn đề của mình. Đây là một ví dụ minh họa việc thêm vào danh sách trong một luồng liên tục và liên tục xóa khỏi danh sách đó trong một luồng khác Phiên bản hoàn hảo chạy dễ dàng trên các số nhỏ hơn nhưng vẫn giữ các số đủ lớn và chạy vài lần và bạn sẽ thấy lỗi

Phiên bản FLAWED

import threading
import time

# Change this number as you please, bigger numbers will get the error quickly
count = 1000
l = []

def add():
    for i in range(count):
        l.append(i)
        time.sleep(0.0001)

def remove():
    for i in range(count):
        l.remove(i)
        time.sleep(0.0001)


t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()

print(l)

Đầu ra khi LRI

Exception in thread Thread-63:
Traceback (most recent call last):
  File "/Users/zup/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/Users/zup/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "<ipython-input-30-ecfbac1c776f>", line 13, in remove
    l.remove(i)
ValueError: list.remove(x): x not in list

Phiên bản sử dụng ổ khóa

import threading
import time
count = 1000
l = []
lock = threading.RLock()
def add():
    with lock:
        for i in range(count):
            l.append(i)
            time.sleep(0.0001)

def remove():
    with lock:
        for i in range(count):
            l.remove(i)
            time.sleep(0.0001)


t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()

print(l)

Đầu ra

[] # Empty list

Phần kết luận

Như đã đề cập trong các câu trả lời trước trong khi hành động nối thêm hoặc bật các phần tử từ danh sách là an toàn cho luồng, điều không an toàn của luồng là khi bạn nối vào một luồng và bật trong một luồng khác


5
Phiên bản có ổ khóa có hành vi tương tự như phiên bản không có ổ khóa. Về cơ bản lỗi đang đến vì nó đang cố gắng loại bỏ thứ gì đó không có trong danh sách, nó không liên quan gì đến an toàn luồng. Hãy thử chạy phiên bản có khóa sau khi thay đổi thứ tự bắt đầu, tức là bắt đầu t2 trước t1 và bạn sẽ thấy lỗi tương tự. Bất cứ khi nào t2 vượt lên trước t1, ​​lỗi sẽ xảy ra bất kể bạn có sử dụng khóa hay không.
Dev

1
Ngoài ra, bạn nên sử dụng trình quản lý bối cảnh ( with r:) thay vì gọi một cách rõ ràng r.acquire()r.release()
GordonAitchJay

1
@GordonAitchJay
Timothy C. Quinn
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.