본문 바로가기

개발하자

python2의 멀티스레딩과 python3의 멀티스레딩

반응형

python2의 멀티스레딩과 python3의 멀티스레딩

저는 파이썬의 GIL에 익숙하기 때문에 멀티스레딩이 파이썬에서 실제로 멀티스레딩이 아니라는 것을 알고 있습니다.

아래 코드를 실행했을 때 GIL이 레이스 조건을 허용하지 않기 때문에 결과가 0이 될 것으로 예상했습니다. python3에서 결과는 0이었다. 그러나 python2에서, 그것은 0이 아니었다; 결과는 -3492나 21283처럼 예상치 못한 것이었다.

어떻게 하면 문제를 해결할 수 있을까요?

import threading 
x = 0 # A shared value

def foo(): 
  global x 
  for i in range(100000000): 
    x += 1 

def bar(): 
  global x 
  for i in range(100000000):
    x -= 1 

t1 = threading.Thread(target=foo) 
t2 = threading.Thread(target=bar) 

t1.start() 
t2.start() 

t1.join() 
t2.join() # Wait for completion

print(x)



이 문은 어떤 버전의 파이썬에서도 스레드 세이프가 아니다. Python 2에서 레이스 조건의 결과를 보고 있었지만 Python 3에서는 보지 않았다는 사실은 대부분 우연에 불과합니다(GIL이 스레드 간에 전환되는 시기에 대한 최적화와 관련이 있을 수 있지만 자세한 내용은 모르겠습니다). Python 3에서도 잘못된 결과를 얻을 수 있습니다.

그 이유는 연산자가 원자가 아니기 때문이다. 실행하기 위해서는 여러 바이트 코드가 필요하며, GIL은 오직 하나의 바이트 코드가 실행되는 동안 스레드 간의 전환을 방지하도록 보장된다. 함수가 어떻게 작동하는지 보기 위해 함수의 분해를 살펴보자. (이것은 파이썬 3.7에서 나온 것으로, 파이썬 2.7에서는 바이트 코드 내의 주소가 다르지만 모든 작업은 동일하다.):

>>> dis.dis(foo)
  3           0 SETUP_LOOP              24 (to 26)
              2 LOAD_GLOBAL              0 (range)
              4 LOAD_CONST               1 (100000000)
              6 CALL_FUNCTION            1
              8 GET_ITER
        >>   10 FOR_ITER                12 (to 24)
             12 STORE_FAST               0 (i)

  4          14 LOAD_GLOBAL              1 (x)
             16 LOAD_CONST               2 (1)
             18 INPLACE_ADD
             20 STORE_GLOBAL             1 (x)
             22 JUMP_ABSOLUTE           10
        >>   24 POP_BLOCK
        >>   26 LOAD_CONST               0 (None)
             28 RETURN_VALUE

우리가 신경 쓰는 라인은 바이트 코드 위치가 14-20인 네 개의 라인이다. 처음 두 개는 인수를 추가에 로드합니다. 세 번째는 수술을 한다. 모든 유형의 개체가 제자리에서 업데이트될 수 있는 것은 아니기 때문에 추가 결과는 스택에 다시 저장됩니다(정수는 업데이트할 수 없으므로 여기서 필요). 마지막 바이트 코드는 합계를 원래 이름으로 다시 저장합니다.

만약 인터프리터가 우리가 바이트코드 14에 로드할 때와 바이트코드 20에 새로운 값을 다시 저장할 때 사이에 GIL을 보유하는 스레드를 전환하기로 선택한다면, 우리는 아마도 잘못된 결과로 끝날 것이다. 왜냐하면 우리가 이전에 로드한 값은 다시 GIL을 보유할 때 더 이상 유효하지 않을 수 있기 때문이다.

위에서 언급했듯이, Python 3에서 얻을 수 있다는 사실은 단순히 구현 세부사항의 결과이며, 인터프리터는 당신이 테스트할 때 바이트 코드의 중요한 섹션에서 전환하지 않기로 선택했습니다. 다른 상황(예: CPU 부하가 큰 경우)이나 다른 인터프리터 버전(예: 3.6 대신 3.7 또는 기타)에서 프로그램을 다시 실행해도 프로그램이 다르게 선택되지 않는다는 보장은 없습니다.

실제 스레드 안전을 원한다면 GIL에만 의존하지 말고 실제 잠금 장치를 사용해야 합니다. GIL은 통역사의 내부 상태가 제정신을 유지하도록 보장할 뿐이다. 코드의 모든 행이 원자적이라는 것을 보장하지는 않습니다.


반응형