쿠키의 저장소

[Python] Python의 GIL과 멀티스레딩 - 2 본문

Language/Python

[Python] Python의 GIL과 멀티스레딩 - 2

모카치노쿠키 2023. 5. 3. 16:46

저번 포스팅(https://dev-jaeho.tistory.com/30)에서는 GIL이 무엇인지 멀티 스레딩과는 어떠한 관계가 있는지, 그래서 파이썬에서는 멀티스레딩이 가능한 것인지에 대해서 살펴보았습니다. 짧게 정리하자면 GIL을 획득한 스레드가 CPU를 점유하게 되며 다른 스레드는 그동안은 작업을 할 수 없습니다. 다만 GIL이 해제되는 시점에 다른 스레드가 GIL을 획득하여 작업을 진행할 수 있게 되므로, IO 바운드 등 CPU 대기 시간이 많이 발생하는 작업을 할 때는 멀티스레딩으로 성능 개선의 효과를 달성할 수 있다는 것이 지난 포스팅의 결론이었습니다.

다만 개인적으로는 멀티 스레딩 방식보다는 멀티 프로세싱 방식을 택하는 것이 더 안정적이라고 판단을 하는 편인데요. 그 원인으로 동시성 이슈를 말씀드렸었습니다. 이번 포스팅에서는 파이썬에서 발생할 수 있는 동시성 이슈에 대해서 서술하도록 하겠습니다.

사실 GIL에 대해서 알아보면서도 계속 이해가 가지 않았습니다. 왜 Segmentation Fault가 발생했을까? GIL 때문에 한 번에 한 스레드 밖에 실행이 되지 않는데... 대체 어떻게? 메모리 이슈가 어떻게 발생할 수 있을까? 이해가 가지 않았는데요. 설명에 앞서 우리의 ChatGPT에게 한 번 물어보도록 하겠습니다.

우리의 ChatGPT는 파이썬에서도 동시성 이슈가 발생할 수 있다고 하네요!
GIL로 보호되고 있는 것 아닌가? 동시성 이슈가 없어야하는 것 아닌지 다시 생각해보고 또 한 번 ChatGPT에게 물어보아도 우리의 ChatGPT는 같은 대답을 들려줍니다. 파이썬에서도 동시성 이슈가 존재한다고 반복해서 말해주네요.

조금 더 자세히 살펴보도록 하겠습니다.
"여러 스레드가 동시에 리스트에 접근하여 항목을 추가하거나 삭제하는 경우, 이러한 작업은 원자적이지 않아서 중간에 다른 스레드가 리스트를 변경할 수 있습니다."
이 내용은 Race Condition 이슈에 관한 내용인데요. 이야기하자면 다음과 같은 예시가 있을 수 있습니다.
스레드 A에서 리스트의 3번 인덱스에 접근하여 해당 요소를 삭제하려 합니다. 그런데 앞서 GIL을 획득했던 스레드 B가 먼저 해당 리스트에 접근하여 이미 3번 인덱스의 요소를 삭제해버렸습니다. 3번 인덱스가 마지막 인덱스였기에 이제 해당 리스트에는 2번 인덱스까지만 존재합니다. 스레드 B가 GIL을 해제하고 스레드 A가 GIL을 이어받아 3번 인덱스에 접근합니다. 그러나 이미 해당 요소는 삭제되어 존재하지 않아 잘못된 메모리에 접근하게 됩니다. 그러면 out of index 에러가 발생하게 되는 것이죠. 이와 같은 상황은 스레드가 점유하고 있던 GIL이 해제되면서 발생할 수 있습니다. GIL이 해제되는 조건에 대해서는 지난 포스팅에서 서술한 바 있으니 참고해주시면 되겠습니다.

또한 일부 자료구조에는 GIL이 적용되지 않습니다. GIL은 파이썬이 관리하는 메모리 자원(객체, 모듈 등)에 대해서 동기화를 보장해줍니다만 C언어로 구현 된 자료구조 및 라이브러리 중 일부는 GIL이 적용되지 않습니다. GIL의 영향을 받지 않는 대표적인 자료구조는 다음과 같습니다.

1. queue 모듈의 Queue, multiprocessing 모듈의 Queue 클래스
2. threading 모듈의 local 클래스
3. array 모듈의 array 클래스
4. multiprocessing 모듈의 Value, Array 클래스
5. numpy
6. pandas
7. ctypes 모듈
8. socket 등 파이썬 네트워크 모듈

위와 같은 모듈들은 내부적으로 자체적인 스레딩 모델을 갖고 있다거나 threading 모듈의 Lock, Condition 등을 사용하여 구현되어 있기 때문에 GIL의 영향을 받지 않습니다. GIL의 영향을 받지않는 자료구조라는 말은 thread-safe하다는 의미이기도 하지만 그렇지 않은 경우도 GIL의 영향을 받지 않는다고 이야기를 합니다. 그 이야기인 즉슨 해당 자료구조들 중 일부는 여러 개의 스레드가 동시에 접근 가능한 자료구조라는 이야기가 됩니다. 이러한 자료구조를 사용하게 되면 race condition이 발생할 수 있는 여지가 남게 됩니다. GIL은 동시에 파이썬의 바이트 코드가 여러 스레드에서 실행되지 않게 해주는 역할을 하는 것이지 스레드의 실행 순서 등을 보장하는 것이 아니기 때문입니다. 따라서 위와 같은 자료구조를 사용하는데 멀티 스레딩을 해야한다면 Lock을 활용한 동시성 제어 로직을 함께 구현해주어야합니다.

이와 같은 맥락에서 파이썬 코드를 작성할 때, 자료구조에 접근해서 추가, 수정, 삭제하는 작업이 빈번한 코드이거나 IO 바운드 작업이 거의 존재하지 않는 코드라고 한다면 굳이 위험하게 동시성 제어 방법을 동원하면서까지 멀티스레딩을 하기보다는 멀티프로세싱을 통하여 안전하게 GIL을 우회하는 것이 낫다고 생각합니다. 게다가 프레임워크나 미들웨어 등 규모가 큰 외부 라이브러리를 사용하게 된다면 해당 라이브러리 내부가 어떻게 구현되어 있을지 모두 파악하는 것이 어렵기도 하고 이것을 고려하여 동기화 기법을 적용하는 것 또한 현실적으로 무리가 있습니다. 저의 경우에도 멀티 스레딩을 시도했다가 ML 쪽 라이브러리 내부에서 동시성 이슈가 있는 것을 발견하고 Lock을 잡는 등 별 일을 다 했는데요. 결론적으로는 멀티 프로세스의 싱글 스레드가 가장 안정적이었고, 특히나 ML 작업같은 헤비한 CPU 바운드 작업의 경우에는 멀티 스레드의 이점이 크게 없기 때문에 굳이 멀티 스레딩을 시도할 이유가 없었습니다.

지금까지의 포스팅 내용을 정리하자면 GIL의 존재와 파이썬의 멀티 스레딩 가능 여부는 별개의 것입니다. GIL은 특정 조건 하에서 해제될 수 있으며 다른 스레드가 획득하여 작업을 수행할 수 있으므로 멀티 스레딩이 가능합니다. 다만 CPU 바운드 작업의 경우 멀티 스레딩의 이득을 볼 수 없으며 IO 바운드 작업의 경우가 멀티 스레딩을 적용하기 좋은 사례입니다. 다만 멀티 스레딩을 적용할 경우, race condition이 발생할 수 있으므로 동시에 접근 가능한 자료구조에 대한 동시성 제어 기법을 적용해주어야 합니다. IO 바운드 작업 부분에는 이벤트 루프 방식을 적용하는 것도 하나의 방법이 될 수 있을 것 같네요. 또한 GIL 자체를 우회하는 방법으로 멀티 스레딩을 활용하지 않고 멀티 프로세싱을 활용하는 것 또한 하나의 좋은 방법이 될 수 있겠습니다. 파이썬의 경우 WSGI, ASGI 등의 인터페이스를 통해 메인 프로세스를 fork 하는 방식으로 효율적인 멀티 프로세싱을 지원하고 있으니 이를 활용해보는 것도 좋겠습니다.

혹여나 잘못된 내용이 있다면 친절하게 댓글로 피드백 남겨주신다면 다시 한 번 검토해보고 수정하도록 하겠습니다.

'Language > Python' 카테고리의 다른 글

[Python] Python의 GIL과 멀티스레딩 - 1  (0) 2023.04.27
Comments