목록은 스레드로부터 안전합니까?


155

목록과 대신에 여러 스레드가있는 대기열을 사용하는 것이 좋습니다 .pop(). 목록이 스레드로부터 안전하지 않거나 다른 이유로 인해 발생합니까?


1
파이썬에서 스레드 안전이 정확히 보장되는 것을 항상 말하기 어렵고, 스레드 안전에 대해 추론하기가 어렵습니다. 매우 인기있는 비트 코인 지갑 일렉트 룸 (Electrum)조차도 동시성 버그가 원인 일 수 있습니다.
sudo

답변:


182

목록 자체는 스레드로부터 안전합니다. CPython에서 GIL은 동시 액세스에 대해 보호하고 다른 구현에서는 목록 구현에 세밀한 잠금 또는 동기화 된 데이터 유형을 사용하도록주의를 기울입니다. 그러나 동시에 액세스하려는 시도로 인해 목록 자체 가 손상 될 수는 없지만 목록의 데이터 는 보호되지 않습니다. 예를 들면 다음과 같습니다.

L[0] += 1

+=원자 연산이 아니기 때문에 다른 스레드가 동일한 작업을 수행하는 경우 실제로 L [0]을 1 씩 증가시킬 수 는 없습니다. (실제로 파이썬에서는 거의 임의의 작업이 원자 적입니다. 대부분의 작업은 임의의 Python 코드를 호출 할 수 있기 때문입니다.) 보호되지 않은 목록을 사용하는 경우 경쟁으로 인해 잘못된 항목을 가져 오거나 삭제할 수 있으므로 큐를 사용해야합니다. 정황.


1
deque도 스레드 안전합니까? 내 용도에 더 적합한 것 같습니다.
lemiant

20
모든 파이썬 객체는 동일한 종류의 스레드 안전성을 가지고 있습니다. collections.deque는 Queue.Queue 객체 뒤에있는 것입니다. 두 개의 스레드에서 액세스하는 경우 실제로 Queue.Queue 객체를 사용해야합니다. 정말.
Thomas Wouters 2018 년

10
lemiant, deque는 스레드로부터 안전합니다. Fluent Python의 2 장에서 : "클래스 collections.deque는 양쪽 끝에서 빠르게 삽입하고 제거 할 수 있도록 설계된 스레드 안전 이중 엔드 큐입니다. [...] append 및 popleft 연산은 원 자성이므로 deque는 안전합니다. 잠금을 사용할 필요없이 멀티 스레드 애플리케이션에서 LIFO 큐로 사용하십시오. "
Al Sweigart

3
CPython 또는 Python에 대한 대답입니까? 파이썬 자체에 대한 답은 무엇입니까?
user541686

@Nils :이 때문에 어, 당신은 연결의 첫 페이지 대신 CPython과의 파이썬을 말한다 되어 파이썬 언어를 설명. 그리고 두 번째 링크는 말 그대로 파이썬 언어의 여러 구현이 있으며 더 인기있는 것입니다. 파이썬에 관한 질문이 있다면 대답은 CPython에서 일어나는 일뿐 만 아니라 모든 적합한 파이썬 구현에서 일어날 수있는 일을 설명해야합니다.
user541686

89

토마스의 우수한 대답 점을 명확히하기 위해, 그 언급해야 append() 입니다 스레드 안전합니다.

데이터 를 쓰려고 하면 읽은 데이터가 같은 위치에있을 염려가 없기 때문입니다 . 작업은 단지 목록에 데이터를 기록 데이터를 읽지 않습니다.append()


1
PyList_Append가 메모리에서 읽습니다. 읽기 및 쓰기가 동일한 GIL 잠금에서 발생한다는 의미입니까? github.com/python/cpython/blob/…
amwinter

1
@amwinter 예, 전체 호출 PyList_Append은 하나의 GIL 잠금으로 수행됩니다. 추가 할 객체에 대한 참조가 제공됩니다. 해당 객체의 내용은 평가 된 후 및 호출 PyList_Append이 완료 되기 전에 변경 될 수 있습니다 . 하지만 여전히 동일한 객체, 안전하게 (당신이 경우에 추가 될 것입니다 lst.append(x); ok = lst[-1] is x, 다음 ok물론, 거짓 일 수 있음). 참조 코드는 INCREF 코드를 제외하고 추가 된 객체에서 읽지 않습니다. 추가 된 목록을 읽고 재 할당 할 수 있습니다.
greggo

3
dotancohen의 점 즉 L[0] += x을 수행 __getitem__L다음 __setitem__L경우 - L지원 __iadd__은 객체 인터페이스에서 다르게 조금을 할 것입니다,하지만 두 개의 별도의 작업이 여전히있다 L파이썬 인터프리터 수준에서 (당신이 그들을 볼 수 있습니다 컴파일 된 바이트 코드). 는 append바이트 코드에서 AA 하나의 메소드 호출로 이루어집니다.
greggo

6
어때요 remove?
열중

2
공감! 하나의 스레드를 지속적으로 추가하고 다른 스레드를 팝업 할 수 있습니까?
PirateApp


2

나는 최근에 하나의 스레드에서 목록에 지속적으로 추가하고, 항목을 반복하고 항목이 준비되었는지 확인하고, 내 경우에는 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)

ERROR시 출력

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

결론

이전 답변에서 언급했듯이 목록 자체에서 요소를 추가하거나 팝하는 작업은 스레드로부터 안전하지만 스레드 안전하지 않은 것은 한 스레드를 추가하고 다른 스레드를 팝할 때입니다


6
잠금이있는 버전은 잠금이없는 버전과 동일한 동작을합니다. 기본적으로 목록에없는 것을 제거하려고하기 때문에 오류가 발생합니다. 스레드 안전과 관련이 없습니다. 시작 순서를 변경 한 후 잠금으로 버전을 실행 해보십시오 (예 : t1 전에 t2를 시작하면 동일한 오류가 표시됨). t2가 t1보다 앞서 갈 때마다 잠금 사용 여부에 관계없이 오류가 발생합니다.
Dev

1
또한, 컨텍스트 관리자를 (사용하는 것이 더 낫다 with r:) 대신 명시 적으로 호출 r.acquire()하고r.release()
GordonAitchJay

1
@GordonAitchJay 👍
Timothy C. Quinn
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.