쿠키의 저장소

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

Language/Python

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

모카치노쿠키 2023. 4. 27. 20:36

이번 포스팅은 Python에 관한 첫 포스팅입니다.
그동안 저는 Java를 주로 사용해왔으며 최근 3년 동안은 Kotlin을 주력으로 사용해왔기에 Python은 사용할 일이 없었습니다. 개인적으로 파이썬의 독특한 문법인 indentation 단위로 함수 범위를 정의하는 부분 때문에 좋아하지 않기도 했었구요. 그렇지만 개발자의 숙명이 다들 그렇지 않겠습니까... 하기 싫다고 안할 수 없는 것이 바로 개발자 아니겠습니까? 원하지는 않았지만 회사에서 Python 코드를 만져야할 일이 있어서 Python을 다루게 되었습니다. 그런데 너무 이상한 일이 발생하는 것이 아니겠어요? 저는 분명히 Python은 GIL이라는 정체모를 것 때문에 싱글 스레드로 동작한다고 알고 있었는데 segmentation fault 오류가 발생하는 것입니다. 보통 segmentation fault는 메모리 문제에서 발생하는데, C언어처럼 메모리 할당부터 직접하는 것도 아닌 Python에서 segmentation fault가 발생한다? 아무리 생각해도 멀티 스레딩 환경에서의 동시성 문제 밖에는 떠오르지 않았습니다(물론 다른 문제도 있을 수 있겠습니다만... 저의 지식 안에서는 떠오르는 것이 없었습니다). 그래서 찾아보았더니 Python에도 멀티 스레딩이 가능하며 Lock 객체를 얻어서 lock을 걸어둘 수가 있더라구요. 그랬더니 문제가 해결되었습니다. 여기서 저의 의문은 발생하였습니다.

"Python은 GIL의 존재로 인해 싱글 스레드로 동작한다고 알고 있었는데, 멀티 스레드도 가능하다고?
Lock도 걸 수 있다고? 그럼 대체 GIL은 뭔데? 멀티 스레딩이 가능하면 GIL이 왜 필요하지?!"

우선 GILGlobal Interpreter Lock의 줄임말입니다. 파이썬 특유의 잠금 메커니즘이죠. 기본적으로 CPython 인터프리터에서 사용하는 Mutex 기반의 메커니즘을 의미합니다. GIL은 동시에 하나의 스레드만 파이썬 바이트코트를 실행할 수 있도록 제한합니다. 즉, 파이썬에서는 동시에 하나의 스레드만 CPU를 사용할 수 있으며, GIL을 획득하지 못한 다른 스레드는 대기하게 됩니다. 이 때, GIL을 획득한 스레드는 기본적으로 자신이 할 일을 전부 처리할 때 까지 GIL을 획득하고 있게 됩니다. 그러니까 이 스레드가 자기가 맡은 일을 완전히 끝내기 전까지는 다른 스레드는 일을 할 수 없다는 것이지요.

그렇다면 여기서 의문 하나!

"파이썬은 멀티 스레딩이 가능한 것이 맞는가?"

위에선 분명히 멀티 스레드가 가능하다면서...? 그런데 GIL을 획득하지 못한 다른 스레드는 CPU를 사용하지 못하고 대기를 하게 된다구? 앞의 스레드가 일을 끝내고 다른 스레드가 일을 시작하면 그게 무슨 멀티 스레드야? 말만 멀티 스레드지 싱글 스레드네?

결론부터 말씀드리자면 파이썬에서는 멀티 스레딩이 가능합니다. 다만 하고자하는 작업의 종류에 따라서 다소 제한적일 수 있습니다. 우선 어떻게 멀티 스레딩이 가능한지를 말씀드리겠습니다. 우리가 기본적으로 알고있는 Mutex의 로직에 의하면 GIL이라는 Key를 소유한 스레드만이 파이썬 바이트코드에 접근이 가능합니다. 바꿔 이야기하면 해당 스레드가 GIL이라는 Key를 놔주게 되면 다른 스레드가 GIL을 획득하여 작업을 수행할 수 있습니다. 앞에서는 스레드가 모든 작업을 완료할 때까지 획득한 GIL을 놓지 않는다고 말씀드렸습니다만, 정확히 말하자면 그렇지는 않습니다. 스레드가 GIL을 놓아주는 것 즉, 스레드의 GIL이 해제되는 조건이 있습니다. 해당 조건을 마주하게 되면 스레드는 GIL을 놓게 되고 다른 스레드가 GIL을 획득할 수 있게 됩니다. GIL이 해제되는 조건은 크게 두 가지가 있습니다.

1. I/O 바운드 작업을 수행할 때, 스레드는 GIL을 해제하고 다른 스레드가 GIL을 획득하여 실행될 수 있게끔 합니다. 이는 보통 I/O 작업은 CPU 대기시간이 많이 발생하는 작업이기 때문입니다. 파일 입출력, DB 작업, 네트워크 통신 등이 대표적인 I/O 바운드 작업에 속합니다.

2. blocking 함수가 호출된 경우에도 GIL이 해제됩니다. 예를 들어 time.sleep() 함수처럼 현재 스레드를 blocking 상태로 만드는 함수가 호출되면 스레드는 GIL을 해제하게 되고 다른 스레드가 GIL을 획득할 수 있는 상태가 됩니다. 외에도 threading.Lock 객체를 직접 얻어내서 명시적으로 GIL을 해제할 수도 있습니다.

위의 조건에 의해 GIL이 해제되는 경우, 다른 스레드가 GIL을 획득 할 수 있고, 멀티 스레딩이 가능해집니다. 다만 CPU 바운드 작업의 경우, GIL이 해제되지 않기 때문에 멀티 스레딩으로 성능 상의 이득을 취할 수 없습니다. 과학 연구나 분석 등에서 사용되는 수학적 계산, 암/복호화 작업, 이미지/영상 처리작업, ML 작업 등이 대표적인 CPU 바운드 작업에 해당합니다. 그러나 일반적인 우리의 개발 업무에서 가장 큰 코스트는 I/O 바운드 작업에서 발생하기 때문에 대부분의 경우 멀티 스레딩을 통해서 성능 상의 이득을 취할 수 있습니다.

그렇다면 CPU 바운드 작업의 경우에는 어떻게 성능을 개선할 수 있을까요? 파이썬에서는 이런 경우를 위해 멀티 프로세싱을 지원하고 이를 이용하도록 권장하고 있습니다. 웹 서비스의 경우에도 WSGI, ASGI 등의 보조를 받아서 메인 프로세스를 포크하는 방식의 멀티 프로세싱 방식을 권장합니다. 멀티프로세싱을 통해 GIL을 우회하는 것이지요.

개인적으로는 멀티 스레딩 방식보다는 멀티 프로세싱 방식을 권장하는데에는 또 다른 이유가 있는데요. 바로 동시성 이슈입니다. 파이썬의 GIL은 동시성 이슈까지는 완벽하게 해결을 해주지 못합니다. 이에 대해서는 다음 포스팅에서 이어가도록 하겠습니다.

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

참고 1) Jython이나 IronPython의 경우, Java 및 .Net 런타임 환경에서 실행되기 때문에 GIL을 사용하지 않는다고 합니다. 또한 PyPy의 경우 GIL을 사용하고 있으나 자체적인 JIT 컴파일러를 갖고있고 GIL을 효율적으로 처리하는 방법을 별도로 갖고있기 때문에 상대적으로 GIL의 영향을 덜 받는다고 합니다. 그래서 본 포스팅에서는 CPython을 기준으로 작성하였습니다.

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

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