Apakah daftar aman untuk thread?

155

Saya perhatikan bahwa sering disarankan untuk menggunakan antrian dengan banyak utas, bukan daftar dan .pop(). Apakah ini karena daftar tidak aman untuk alasan lain, atau karena alasan lain?

lemiant
sumber
1
Sulit untuk mengatakan selalu apa yang sebenarnya dijamin aman-threaded di Python, dan sulit untuk alasan tentang keamanan thread di dalamnya. Bahkan dompet Bitcoin Electrum yang sangat populer telah memiliki bug konkurensi yang kemungkinan berasal dari ini.
sudo

Jawaban:

182

Daftar itu sendiri aman untuk thread. Dalam CPython, GIL melindungi terhadap akses bersamaan untuk mereka, dan implementasi lainnya berhati-hati untuk menggunakan kunci berbutir halus atau tipe data yang disinkronkan untuk implementasi daftar mereka. Namun, sementara daftar sendiri tidak bisa pergi korup dengan upaya untuk akses bersamaan, daftar ini data yang tidak dilindungi. Sebagai contoh:

L[0] += 1

tidak dijamin untuk benar-benar meningkatkan L [0] oleh satu jika thread lain melakukan hal yang sama, karena +=bukan merupakan operasi atom. (Sangat, sangat sedikit operasi di Python sebenarnya atom, karena sebagian besar dari mereka dapat menyebabkan kode Python sewenang-wenang disebut.) Anda harus menggunakan Antrian karena jika Anda hanya menggunakan daftar yang tidak dilindungi, Anda mungkin mendapatkan atau menghapus item yang salah karena ras kondisi.

Thomas Wouters
sumber
1
Apakah deque juga aman? Tampaknya lebih tepat untuk saya gunakan.
lemiant
20
Semua objek Python memiliki jenis keamanan yang sama - mereka sendiri tidak rusak, tetapi datanya mungkin. collections.deque adalah apa yang ada di balik objek Queue.Queue. Jika Anda mengakses sesuatu dari dua utas, Anda harus menggunakan objek Antrian. Betulkah.
Thomas Wouters
10
lemiant, deque aman untuk benang. Dari Bab 2 Fluent Python: "The class collections.deque adalah antrian ujung ganda aman-benang yang dirancang untuk memasukkan dan melepas dengan cepat dari kedua ujungnya. [...] Operasi append dan popleft bersifat atomik, sehingga deque aman untuk gunakan sebagai antrian LIFO dalam aplikasi multi-utas tanpa perlu menggunakan kunci. "
Al Sweigart
3
Apakah jawaban ini tentang CPython atau tentang Python? Apa jawaban untuk Python itu sendiri?
user541686
@Nils: Uh, halaman pertama yang Anda tautkan ke mengatakan Python, bukan CPython karena ini menggambarkan bahasa Python. Dan tautan kedua itu secara harfiah mengatakan ada beberapa implementasi dari bahasa Python, hanya satu yang kebetulan lebih populer. Mengingat pertanyaannya adalah tentang Python, jawabannya harus menjelaskan apa yang dapat dijamin terjadi dalam implementasi Python yang sesuai, bukan hanya apa yang terjadi pada CPython pada khususnya.
user541686
89

Untuk memperjelas titik dalam jawaban Thomas' baik, harus disebutkan bahwa append() adalah thread aman.

Ini karena tidak ada kekhawatiran bahwa data yang sedang dibaca akan berada di tempat yang sama setelah kita menulisnya . The append()operasi tidak membaca data, hanya menulis data ke dalam daftar.

dotancohen
sumber
1
PyList_Append membaca dari memori. Apakah maksud Anda bahwa membaca dan menulis terjadi dalam kunci GIL yang sama? github.com/python/cpython/blob/…
amwinter
1
@ amwinter Ya, seluruh panggilan ke PyList_Appenddilakukan dalam satu kunci GIL. Itu diberikan referensi ke objek untuk ditambahkan. Konten objek itu dapat diubah setelah dievaluasi dan sebelum panggilan ke PyList_Appenddilakukan. Tapi itu akan tetap menjadi objek yang sama, dan ditambahkan dengan aman (jika Anda melakukannya lst.append(x); ok = lst[-1] is x, maka okmungkin False, tentu saja). Kode yang Anda referensi tidak membaca dari objek yang ditambahkan, kecuali untuk INCREF. Bunyinya, dan mungkin merealokasi, daftar yang ditambahkan.
greggo
3
Titik dotancohen adalah yang L[0] += xakan melakukan __getitem__on Ldan kemudian __setitem__on L- jika Lmendukungnya __iadd__akan melakukan sesuatu yang sedikit berbeda pada antarmuka objek, tetapi masih ada dua operasi terpisah pada Llevel interpreter python (Anda akan melihatnya di dikompilasi dengan bytecode). Hal appendini dilakukan dalam pemanggilan metode tunggal dalam bytecode.
greggo
6
Bagaimana dengan remove?
acrazing
2
terbalik! jadi bisakah saya menambahkan satu utas terus-menerus dan muncul di utas lain?
PirateApp
2

Saya baru-baru ini memiliki kasus ini di mana saya perlu menambahkan daftar terus-menerus dalam satu utas, loop melalui item dan memeriksa apakah item sudah siap, itu adalah AsyncResult dalam kasus saya dan menghapusnya dari daftar hanya jika sudah siap. Saya tidak dapat menemukan contoh yang menunjukkan masalah saya dengan jelas. Berikut adalah contoh yang menunjukkan penambahan ke daftar di satu utas secara terus-menerus dan menghapus dari daftar yang sama di utas lain terus-menerus beberapa kali dan Anda akan melihat kesalahan

Versi cacat

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)

Output saat KESALAHAN

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

Versi yang menggunakan kunci

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)

Keluaran

[] # Empty list

Kesimpulan

Seperti disebutkan dalam jawaban sebelumnya sementara tindakan menambahkan atau muncul elemen dari daftar itu sendiri adalah thread aman, yang tidak aman thread adalah ketika Anda menambahkan satu thread dan pop di yang lain

PirateApp
sumber
6
Versi dengan kunci memiliki perilaku yang sama seperti versi tanpa kunci. Pada dasarnya kesalahan datang karena mencoba menghapus sesuatu yang tidak ada dalam daftar, tidak ada hubungannya dengan keamanan utas. Coba jalankan versi dengan kunci setelah mengubah urutan mulai yaitu mulai t2 sebelum t1 dan Anda akan melihat kesalahan yang sama. setiap kali t2 lebih cepat dari t1 kesalahan akan terjadi tidak peduli apakah Anda menggunakan kunci atau tidak.
Dev
1
Selain itu, Anda lebih baik menggunakan manajer konteks ( with r:) daripada menelepon secara eksplisit r.acquire()danr.release()
GordonAitchJay
1
@GordonAitchJay 👍
Timothy C. Quinn