リストはスレッドセーフですか?


154

リストやではなく、複数のスレッドを持つキューを使用することが推奨されることがよくあります.pop()。これは、リストがスレッドセーフではないためか、その他の理由でですか?


1
Pythonでスレッドセーフが保証されているものを正確に常に知るのは難しく、スレッドセーフについて論理的に判断するのは困難です。非常に人気の高いビットコインウォレットElectrumでさえ、これに起因する可能性のある同時実行性のバグがありました。
sudo

回答:


181

リスト自体はスレッドセーフです。CPythonでは、GILはそれらへの同時アクセスから保護します。他の実装では、リストの実装に細粒度ロックまたは同期データ型を使用するように注意しています。ただし、リスト自体は同時にアクセスしようとしても破損することはありませんが、リストのデータは保護されません。例えば:

L[0] += 1

+=アトミック操作ではないため、別のスレッドが同じことを行った場合、実際にL [0]を1だけ増やすことは保証されません。(非常に少数のPythonの操作は実際にはアトミックです。それらのほとんどは任意のPythonコードが呼び出される可能性があるためです。)保護されていないリストを使用するだけの場合、競合のために間違ったアイテムを取得または削除する可能性があるため、キューを使用する必要があります。条件。


1
dequeもスレッドセーフですか?それは私の使用により適しているようです。
レミアント

20
すべてのPythonオブジェクトには同じ種類のスレッドセーフ性があります-それら自体は破損しませんが、データは破損する可能性があります。collections.dequeは、Queue.Queueオブジェクトの背後にあるものです。2つのスレッドからアクセスしている場合は、Queue.Queueオブジェクトを使用する必要があります。本当に。
Thomas Wouters、2011年

10
lemiant、dequeはスレッドセーフです。Fluent Pythonの第2章から:「クラスcollections.dequeは、両端からの高速の挿入と削除のために設計されたスレッドセーフなダブルエンドキューです。[...] appendおよびpopleft操作はアトミックであるため、dequeは安全ですロックを使用する必要なしに、マルチスレッドアプリケーションでLIFOキューとして使用します。」
Al Sweigart

3
これはCPythonに関するものですか、それともPythonに関するものですか?Python自体の答えは何ですか?
user541686

@ニルス:ええと、リンクした最初のページ、Python言語を記述しているため、CPythonではなくPythonと言っています。そして、その2番目のリンクは、Python言語の複数の実装があることを文字通り示しています。質問はPythonに関するものだったので、答えは、特にCPythonで何が起こるかだけでなく、Pythonの適合実装で何が起こることが保証できるかを説明する必要があります。
user541686

89

Thomasの優れた答えのポイントを明確にするために、スレッドセーフであることに言及する必要がappend() あります。

これは、一度書き込んデータが読み込まれると同じ場所にあるという心配がないためです。操作はそれだけでリストにデータを書き込み、データを読み取ることはありません。append()


1
PyList_Appendがメモリから読み込んでいます。読み取りと書き込みが同じGILロックで行われるということですか?github.com/python/cpython/blob/...
amwinter

1
@amwinterはい、呼び出し全体PyList_Appendが1つのGILロックで行われます。追加するオブジェクトへの参照が与えられます。そのオブジェクトの内容は、評価された後、呼び出しPyList_Appendが行われる前に変更される可能性があります。しかし、それはまだ同じオブジェクト、および安全(あなたがしなければ追加されますlst.append(x); ok = lst[-1] is x、そしてokもちろん、Falseの場合もあります)。参照するコードは、INCREFを除いて、追加されたオブジェクトからは読み取りません。追加されたリストを読み取り、再割り当てします。
greggo 2016年

3
dotancohenのポイントは、つまりL[0] += x実行する__getitem__上でL、その後__setitem__Lあれば- Lサポート__iadd__は、オブジェクト・インターフェースで違うことを少し行いますが、二つの別々の操作がで残っているLPythonインタプリタのレベルで(あなたがそれらを見ることができますコンパイルされたバイトコード)。これappendは、バイトコードの単一のメソッド呼び出しで行われます。
greggo 2016年

6
いかがremoveですか?
acrazing

2
賛成です!それで、あるスレッドに継続的に追加して別のスレッドにポップできますか?
PirateApp 2019


3

私は最近、1つのスレッドで継続的にリストに追加し、アイテムをループしてアイテムの準備ができているかどうかを確認する必要があり、私の場合はAsyncResultであり、準備ができている場合にのみリストから削除する必要がありました。問題を明確に示す例は見つかりませんでした。あるスレッドのリストに継続的に追加し、別のスレッドの同じリストから継続的に削除する例を次に示します。欠陥のあるバージョンは、小さい数で簡単に実行できますが、数を十分に大きくして数回、あなたはエラーが表示されます

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)

エラー時に出力

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

ロックを使用するバージョン

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)

出力

[] # Empty list

結論

以前の回答で述べたように、リスト自体から要素を追加またはポップする動作はスレッドセーフですが、スレッドセーフでないのは、あるスレッドに追加して別のスレッドにポップする場合です。


5
ロック付きバージョンは、ロックなしバージョンと同じ動作をします。リストにないものを削除しようとしているため、基本的にエラーが発生しています。スレッドの安全性とは何の関係もありません。開始順序を変更した後、ロック付きのバージョンを実行してください。つまり、t1の前にt2を開始すると、同じエラーが表示されます。ロックを使用するかどうかに関係なく、t2がt1よりも先に進むたびにエラーが発生します。
Dev

1
また、with r:明示的にr.acquire()andを呼び出すのではなく、コンテキストマネージャ()を使用する方がよいr.release()
GordonAitchJay

1
@GordonAitchJay👍
ティモシーC.クイン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.