GIL, Java에는 없던데?

litien·2020년 5월 1일
4
post-thumbnail

GIL에 대해 조금 찾아보다 Java에서는 왜 GIL에 대한 개념이 없는지 궁금했다. Java 역시 세부적인 동작 방식에는 차이가 있지만 Python과 같이 인터프리터를 사용한다. 그럼에도 불구하고 Java를 공부할 때는 GIL에 대한 언급을 찾아볼 수 없었다. 이번 포스팅에서는 Java에는 GIL이 없는 이유에 대해 간단하게 알아보자.

Python은 언어에 대한 인터페이스이다. Python의 구현체로 Cpython, ironPython, Jython 등이 있으며, 이번 글은 Cpython을 중심으로 다룬다. ironPython과 Jytphon은 GIL 방식을 선택하지 않았다.

GIL란?

GIL은 Global Interpreter Lock의 줄임말로, Python Wiki에서는 다음과 같이 정의하고 있다.

In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. This lock is necessary mainly because CPython's memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

위 내용을 요약하자면 다음과 같다

GIL은 Cpython이 Thread Safe 하지 않기때문에 멀티 스레드 환경에서 여러 파이썬 오브젝트들이 동시에 접근하는 것을 보호하기 위한 mutex이다.

이를 통해 Cpython에서는 여러 스레드들이 동시에 접근하여 발생 할 수 있는 치명적인 문제가 있음을 알 수 있다. 멀티 스레드 환경에서는 다양한 문제가 발생 할 수 있으나, 특이한 점은 유저의 코드로 발생하는 race condition이 아닌 Python 구현체인 Cpython 구현 자체에서 race condition이 발생 할 수 있다는 점이다.

Cpython은 왜 GIL을 선택했는가?

GIL 제약을 선택함으로 Cpython에서는 오직 하나의 스레드만이 인터프리터에 대한 점유권을 획득한다. 때문에 멀티 스레드 환경에서 서로 Lock에 대한 점유권을 획득하는 과정에서 Bottleneck이 될 수 있다. Bottleneck되지 않더라도 성능을 떨어뜨릴 수 있다.

I/O Bound Task에 대한 동시성은 GIL의 영향을 받지 않는다. 단 CPU Bound Task에 대해 동시성/병렬성이 필요한 경우에는 multi processing을 사용하는 것이 좋다.

이러한 성능 이슈에도 불구하고 Cpython이 언어 스펙 측면에서 GIL이라는 제약을 가져간 이유에 대해 이해하기 위해서는 Cpython가 메모리를 어떻게 관리하는지 대한 이해가 필요하다.

Cpython에서는 메모리를 관리하는 방법으로 Reference Counting 을 이용한다. Refrence Counting은 오브젝트마다 자신을 참조하고 있는 변수의 수를 저장한다. 이 수가 0이 되었을 때 Garbage로 판단하고 메모리 해제를 진행한다.

이러한 방식의 Reference Counting은 Garbage를 메모리에 쌓아놓지 않고 곧 바로 해제 할 수 있다는 점에서 메모리 공간 측면의 이점이 있다.

반면에 참조가 해제되거나 추가될 때마다 실시간으로 Reference Count가 동기화 되어야한다. Reference Count를 업데이트하는 작업은 Atomic한 연산이 되어야 한다. 그렇지 않다면 여러 스레드에서 동시에 접근해 원치 않는 결과가 나올 수 있다.

Reference Count 업데이트 연산의 Atomic을 보장하기 위해서는 매 연산마다 Lock을 걸어줘야 하는데, 이는 성능에 상당한 영향을 미치며 최악의 경우에는 Dead Lock을 야기시킬 수 있다. 때문에 Cpython에서는 인터프리터 전체에 Lock을 걸어 Atomic을 보장했다.

Reference Counting은 순환참조하는 객체에 대해서는 탐지가 불가능하다. 때문에 Python에서는 메인으로 Reference Counting을 사용하되 순환참조 탐지를 위해 GC를 보조적으로 사용한다. 이에 대한 자세한 내용은
Python GC가 작동하는 원리를 참고하자

Java에는 왜 GIL이 없는가?

Java는 Mark and Sweep(GC)으로 메모리를 관리한다. 객체 참조에 대한 수를 저장하지 않고, 루트부터 하나씩 참조를 따라가면서 찾은 객체가 살아있음을 Marking 한 후 전체 힙 객체들을 순회하면서 마킹되지 않은 객체들을 제거하는 방식이다.

Mark and Sweep에서는 메모리가 일정수준 이상 찼을 때 위에서 언급한 컬렉팅 작업이 시작된다. 때문에 오브젝트의 참조가 변경 될 때마다 Atomic한 연산이 수행 될 필요가 없다. 이것이 Java에서 GIL라는 용어를 찾아보기 힘든 이유이다. 대신에 참조를 확인하는 mark 과정에서 모든 스레드가 일시적으로 중단시켜 gc의 atomic을 보장한다. 이 시기를 stop the world라 부르는데 이 stop the world를 줄이기 위해 여러 GC 알고리즘들이 발전하고 있다.

Mark and Sweep, Reference Counting

Mark and Sweep은 여러 스레드가 인터프리터 접근 하는 것을 막지 않는다. 그렇다면 Mark and Sweep이 무조건 Referecne Counting보다 좋은가? 그건 아니다. 모든 것은 트레이드 오프다. Mark and Sweep은 일반적인 상황에서 스레드의 동시적인 접근이 가능하다는 장점이 있으나, Garbage를 곧바로 회수하는 것이 아닌 메모리에 쌓아놓기 때문에 메모리 공간 측면에서 단점이 있다. 또한 객체가 소멸되는 시점을 예측하기 힘들다.

즉 객체의 소멸시점이 예측이 되야되는 경우 또는 메모리 공간 사용의 제약이 있는 경우는 Reference Counting이 유리하다.

Cpython은 왜 Reference Counting을?

Cpython이 Reference Counting을 채택한 배경에는 C로 구현된 언어인만큼 C의 영향이 큰 것으로 보인다. 아래는 why does python use both reference counting and mark-and-sweep for gc? 라는 StakcOverFlow의 질문 답 중 일부이다.

Main reason CPython uses it is historical. Originally there was no garbage collection for cyclic objects so cycles led to memory leaks. The C APIs and data structures are based heavily around the principle of reference counting. When real garbage collection was added it wasn't an option to break the existing binary APIs and all the libraries that depended on them so the reference counting had to remain.

Reference

Python Wiki
Python GC가 작동하는 원리
왜 Python에는 GIL이 있는가
why does python use both reference counting and mark-and-sweep for gc?

profile
어려운 문제를 함께 풀어가는 것을 좋아합니다.

0개의 댓글