Listeler iş parçacığı için güvenli midir?


155

Sık sık listeler yerine birden çok iş parçacığı ile kuyruk kullanılması önerilmektedir .pop(). Bunun nedeni listelerin iş parçacığı açısından güvenli olmaması veya başka bir nedenle mi?


1
Python'da her zaman tam olarak güvende olan garantili şeyin ne olduğunu söylemek zor ve içinde iplik güvenliği hakkında akıl yürütmek zor. Son derece popüler Bitcoin cüzdanı Electrum bile, muhtemelen bundan kaynaklanan eşzamanlılık hatalarına sahipti.
sudo

Yanıtlar:


182

Listelerin kendisi iş parçacığı için güvenlidir. CPython'da GIL, onlara eşzamanlı erişimlere karşı koruma sağlar ve diğer uygulamalar, liste uygulamaları için ince taneli bir kilit veya senkronize bir veri türü kullanmaya özen gösterir. Ancak, listelerin kendileri eşzamanlı olarak erişim girişimleriyle bozulamazken, listenin verileri korunmaz. Örneğin:

L[0] += 1

+=atomik bir işlem olmadığı için başka bir iplik aynı şeyi yaparsa, L [0] değerini bir arttırması garanti edilmez. (Python'da çok, çok az işlem aslında atomiktir, çünkü bunların çoğu rastgele Python kodunun çağrılmasına neden olabilir.) Kuyrukları kullanmalısınız çünkü sadece korumasız bir liste kullanırsanız , yarış nedeniyle yanlış öğeyi alabilir veya silebilirsiniz koşullar.


1
Deque ayrıca diş için güvenli midir? Benim kullanımım için daha uygun görünüyor.
lemiant

20
Tüm Python nesneleri aynı tür iş parçacığı güvenliğine sahiptir - kendileri bozulmaz, ancak verileri olabilir. collections.deque Queue.Queue nesnelerinin arkasındadır. İki iş parçacığından bir şeylere erişiyorsanız, gerçekten Queue.Queue nesnelerini kullanmalısınız. Gerçekten mi.
Thomas Wouters

10
lemiant, deque iplik korumalıdır. Fluent Python'un 2. Bölümünden: "class collections.deque, her iki uçtan hızlı bir şekilde takmak ve çıkarmak için tasarlanmış, iplik güvenli bir çift uçlu kuyruktur. [...] Ekleme ve popleft işlemleri atomiktir, bu nedenle deque kilit kullanmaya gerek kalmadan çok iş parçacıklı uygulamalarda LIFO kuyruğu olarak kullanın. "
Al Sweigart

3
Bu cevap CPython veya Python hakkında mı? Python'un kendisi için cevap nedir?
user541686

@Nils: çünkü Uh, bağlantılı ilk sayfa yerine CPython ait Python diyor edilir Python dilini anlatan. Ve bu ikinci bağlantı, kelimenin tam anlamıyla, Python dilinin birden çok uygulaması olduğunu söylüyor, sadece daha popüler olanı. Sorunun Python ile ilgili olduğu düşünüldüğünde, cevap sadece CPython'da olanları değil, Python'un herhangi bir uyumlu uygulamasında neler olacağının garanti edilebileceğini açıklamalıdır.
user541686

89

Thomas'ın mükemmel cevabındaki bir noktayı açıklığa kavuşturmak için append() , ipliğin güvenli olduğu belirtilmelidir .

Bunun nedeni , yazmaya gittiğimizde okunan verilerin aynı yerde olacağına dair bir endişe olmamasıdır. İşlem veri okumaz, sadece listeye veri yazar.append()


1
PyList_Append bellekten okuyor. Yani okuma ve yazma işlemleri aynı GIL kilidinde gerçekleşiyor mu? github.com/python/cpython/blob/…
amwinter

1
@amwinter Evet, tüm çağrı PyList_Appendbir GIL kilidinde yapılır. Eklenecek nesneye bir başvuru verilir. Söz konusu nesnenin içeriği değerlendirildikten ve çağrı PyList_Appendyapılmadan önce değiştirilebilir . Ama yine de aynı nesne ve güvenli bir şekilde (bunu yaparsanız eklenmiş olacak lst.append(x); ok = lst[-1] is x, o zaman oktabii ki, yanlış olabilir). Başvurduğunuz kod, INCREF dışında, eklenen nesneden okumaz. Eklenen listeyi okur ve yeniden tahsis edebilir.
greggo

3
dotancohen'in amacı, L[0] += xbir __getitem__açık Lve sonra bir __setitem__açık gerçekleştirecek L- eğer Ldestekliyorsa __iadd__, nesne arayüzünde işleri biraz farklı yapacak, ancak Lpython yorumlayıcı düzeyinde hala iki ayrı işlem var (bunları derlenmiş bayt kodu). appendByte aa tek yöntem çağrısı yapılır.
greggo

6
Nasıl remove?
17'de

2
upvoted! bu yüzden sürekli olarak bir iş parçacığı ekleyebilir ve başka bir iş parçacığı pop?
PirateApp


2

Geçenlerde bir iş parçacığında sürekli bir listeye eklemek, öğeler arasında döngü ve öğenin hazır olup olmadığını kontrol etmek için gerekli bir durum vardı, benim durumumda bir AsyncResult oldu ve sadece hazır olduğunda listeden kaldırmak. Sorunumu açıkça gösteren herhangi bir örnek bulamadım Burada sürekli bir iş parçacığına listeye ekleme ve sürekli başka bir iş parçacığında aynı listeden kaldırma gösteren bir örnek Kusurlu sürümü daha küçük sayılar üzerinde kolayca çalışır, ancak sayıları yeterince büyük tutmak ve çalıştırmak birkaç kez ve hatayı göreceksiniz

FLAWED sürümü

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)

ERROR olduğunda çıktı

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

Kilitleri kullanan sürüm

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)

Çıktı

[] # Empty list

Sonuç

Önceki yanıtlarda belirtildiği gibi, listenin kendisine öğe ekleme veya patlama eylemi iş parçacığı için güvenliyken, iş parçacığı için güvenli olmayan bir iş parçacığı eklediğinizde ve başka bir iş parçacığında pop yaptığınız zaman


6
Kilitli sürüm, kilitsiz olanla aynı davranışa sahiptir. Temelde hata, listede olmayan bir şeyi kaldırmaya çalıştığı için geliyor, iş parçacığı güvenliği ile ilgisi yok. Başlangıç ​​sırasını değiştirdikten sonra sürümü kilitlerle çalıştırmayı deneyin, yani t1'den önce t2'yi başlatın ve aynı hatayı göreceksiniz. t2, t1'in önüne çıktığında, kilit kullanırsanız veya kullanmazsanız hata oluşur.
Dev

1
Ayrıca, with r:açıkça r.acquire()r.release()
aramak

1
@GordonAitchJay 👍
Timothy C. Quinn
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.