python gil이 있으니까 thread safe 할까요?

OS/이론 2021. 5. 22. 23:16

 multithreading을 공부하시다 보면, GIL에 대해 한 번 정도는 들어보시게 됩니다. Global Interpreter Lock의 줄임말입니다. 그런데 이것이 있으면 Lock이 필요 없을까요? 관련 질문을 봐도, 답은 한결같이 아닐 수도 있다는 것이였습니다.

 

 

[관련글]

원자성을 만족하는 연산을 n번 호출하는 연산은 원자적일까요?

 

 

 이 문서를 읽어 보시면, 각각의 byte code가 어쩌고 저쩌고 언급을 하다가 atomic 하다는 언급을 하고 있어요.

 

 


 아래 예제를 보도록 하겠습니다.

 

 foo는 global 변수 x에 diff를 100번 더합니다.

 

 main 부분을 보겠습니다. diff가 1, -1, 1, -1이 들어감을 알 수 있어요. 18번째 줄에 target이 foo 메서드이고, args가 li의 원소들임을 보면 알 수 있습니다. 각 쓰레드마다 diff 값은 1, -1, 1, -1이 들어가는데요. 정상적으로 수행이 되었다면, 0이라는 값이 찍혀야 할 겁니다. 그런데 이상한 값이 찍히는 경우가 있어요.

 

 

 -100? 이는 73553번째 테스트에서 -100이 찍혔다는 의미입니다.

 

  100과 -100이 찍힌 경우도 있었습니다. 매우 드문 케이스이지만, 0이 아닌 값이 나왔다는 의미는 스레드 안전하지 않다는 의미입니다. range(100)을 range(1)로 바꿔봐도 마찬가지 현상이 나타납니다. 왜 그럴까요?

 

 


 관련 글을 보셨다면 눈치를 채셨을지도 모르겠지만 atomic한 연산을 n번 호출한 것은 atomic 하지 않기 때문에 벌어진 일입니다. 무슨 말인지 dis 모듈로 뜯어보도록 하겠습니다. 문제가 발생한 것은 foo 메서드인 듯 싶으니, foo 메서드만 뜯어보도록 하겠습니다.

 

 

 dis 모듈의 Bytecode를 써 보겠습니다. 인자로 foo를 넣었는데요. 함수를 의미합니다.

 

 

 foo의 일부 바이트 코드만 가지고 온 것인데요. GLOBAL 변수 x를 가져오고, ADD 1을 하고, 다시 STORE GLOBAL하는 과정을 거침을 알 수 있어요. 이 셋이 다 atomic 하다고 하면, 저 연산들을 수행하는 도중에는 다른 쓰레드가 낄 일이 없을 겁니다. 문제는, 각각의 연산들이 끝났을 때, 그 찰나의 순간에 다른 쓰레드가 끼어 들 수도 있다는 것입니다. toctou도 비슷한 원리로 이루어 지고요. 관심이 있으시다면 이 글을 참고해 보시는 것도 좋을 듯 싶습니다.

 

 

 예를 들자면 이런 경우가 생길 수 있어요. 바이트 코드 단위로 묶여 있다면 충분히 나올 수 있는 상황입니다. 아니면, 아래와 같은 상황도 충분히 가능할 겁니다.

 

 

 여기서 군청색은 스레드 1, 노란색은 스레드 2가 해당 바이트 코드를 실행했다는 것을 의미합니다. 이 때, load GLOBAL이 문제인데요. 문서에 따르면, 글로벌 변수의 값을 STACK에 끌고 온다고 되어 있어요.

 

 

 그러면 상황은 위와 같이 도식화를 시킬 수 있어요. 이 상황에서 두 번째 쓰레드가 load x를 하게 되면, 상황은 아래와 같이 됩니다.

 

 

 이렇게 되면, 상황이 다이나믹하게 돌아갑니다. binary_add는 stack에서 돌아가는 연산이니, 1번 쓰레드가 add 1을 하면 stack의 x에는 1이 저장될 거고, 2번 쓰레드가 add -1을 하면 stack의 x에는 -1이 저장될 겁니다. store을 해 버리면 어떨까요? 여기에서는 노란색의 store가 더 뒤에 일어나니, 아래와 같이 될 겁니다.

 

 

 1 + -1은 0인데, -1이 되어 버린 상황이네요. 이것은 atomic한 연산 여러개는 atomic하지 않아서 벌어진 일입니다. bytecode에 대한 정보들은 공식 문서를 참고하셔도 되고, 여기를 참고하셔도 괜찮을 듯 싶네요.

 

 


 그러면 어떻게 하면 좋을까요?

 

 

 가능한 방법 중 하나는, lock을 걸어주면 됩니다. 여기서 문제가 되는 것은 global x가 문제가 되는 것이였습니다. 여기에 여러 쓰레드가 접근해서, x + diff를 하는 것이 문제였는데요. 아예 foo 함수에 진입했을 때, lock을 걸어버리고, 다 끝났을 때 lock을 해제하면 됩니다.

 

 

 그러면 아까와 다르게 -100이나 100이 출력되지 않음을 알 수 있습니다.