저는 최근에 한번도 사용해보지 않았던 Python
을 이용해 일을 하고 있습니다.
저는 .NET
프로그래머로써 이런 생각을 해 보았습니다.
Python
을 사용하면서 CPU 집약적인 일에 병렬적으로 작업하면 좋지 않을까?
결론적으로 Python
에선 불가능한 일이였습니다. 물론 Python
에도 async
, await
를 이용한 비동기 작업은 있지만 이 비동기 작업은 병렬적인 상황이 아닌 동기적인 상황에서만 우리가 예상했던 대로 동작합니다. 간단하게 얘기하면
Python
은 동기적 상황에서 비동기를 지원하지만 병렬적 상황은GIL
로 인해 처리가 불가하다.
그럼 도대체 GIL이 무엇인지 알아봅시다. Global Interpreter Lock
이란 뜻으로
GIL
은 파이썬의 객체가 레퍼런스 카운팅으로 관리되는 상황에서, 여러 스레드가 동시에 같은 객체를 수정하는 것을 막기 위해 인터프리터 전체에 하나의 큰 락을 거는 방식입니다.
GIL
은 프로그램 전체에서 동시에 1개의 일만 진행하게 만다는 것입니다. 그 이유는 파이썬의 객체를 초쇠한의 GC
호출로 레퍼런트 카운트가 0이 되는 순간 메모리에서 삭제하기 위함입니다. 이건 구현도 쉽고 실시간으로 메모리를 삭제할 수 있는 장점이 있습니다.
제가 아는 C#
이나 C++
에는 GIL
이 없습니다. 그래서 가장 처음에 의문을 가졌던 병렬적인 동작이 안됐던 Python
과 다르게 병렬적인 처리가 가능해 이런 면에서는 더 높은 성능을 보일 수 있습니다. 그럼 여기서 궁금증이듭니다.
왜..
GIL
은 파이썬에만 있는거야?
GIL
은 파이썬에만 있지 않다.알아본 결과 GIL
은 Python
에만 있는 것이 아니였고 Ruby
, R
과 같은 다른 인터프리터
언어들에도 있는 개념이였습니다. 여기서 인터프리터
라는 용어가 나왔습니다. GIL
은 다른 인터프리터
언어에도 있는 개념이라고 합니다! 왜죠..??
GIL
이 필요한 이유제가 사용해 보았던 C#
이나 C++
은 약간의 차이는 있지만 컴파일 되는 언어입니다. 인터프리터와는 상반된 개념이죠. 컴파일이 되는 언어는 컴파일 타임에 함수나 변수 등에 대해서 많은 정보를 취합할 수 있습니다. 이 변수는 스택
에 저장하고 이 변수는 얼마나 들어올지 모르는 동적으로 변하는 변수니까 힙
에 할당해야되겠다 라는 것 등을 말이죠.
하지만 인터프리터는 컴파일을 하지 않는 언어이기 때문에 모든 변수가
힙
에 저장 됩니다.
x = 42
이 코드의 메모리 생성 관점을 도표로 보면 아래와 같습니다.
스택: 힙:
+-----------------+ +------------------+
| x (변수) | | 정수 객체 42 |
| → 0x12345678 | --> | (메모리 주소:|
+-----------------+ | 0x12345678) |
+------------------+
이런 식으로 실제 데이터는 42
는 힙에 저장되고 이것을 받는 변수 x
에 힙에 있는 레퍼런스
를 담는 방식입니다. 모든 Python
변수의 메모리 할당은 이렇게 이루어집니다.
이 말은 메모리를 지우기 위해서 모든 변수의 레퍼런스 카운트를 체크해야 한다는 말입니다. 이런 상황에서 모든 변수에 각각 lock
을 걸어서 race condition
없이 카운트를 관리하는 것은 크나큰 낭비일 것입니다.
어차피 모든 변수에
lock
을 걸어서 관리할거면 프로그램 전체적으로 동시에는 단 1개의 쓰레드만 일하도록 하여서race condition
을 없애버리는게 성능에 좋겠다! 라고 그 당시에는 생각했던 것 같습니다.
Python
이 개발 될 당시에 이런 인터프리터
의 메모리 관리 방식에 대한 해법은 GIL
을 만드는 것이였던 것 같습니다. 이제 인터프리터 언어
와 컴파일 언어
의 차이점 하나를 더 알아보았습니다.