Python - GIL - GC

우야·2021년 7월 15일
0

Python을 공부하다 보면 GIL이라는 것이 나온다. 이건 뭐지?

배경 : Python은 Interpreter 언어 이다.

  1. GIL 이란?
  • Global Interpreter Lock의 줄임말로 CPython에서 사용된다.
    • 참고로 Python의 구현체는 Cpython, ironPython, Jython ...등이 있다.
  • GIL은 Cpython이 Thread Safe하지 않기 때문에 멀티 스레드 환경에서 여러 파이썬 오브젝트들이 동시에 접근하는 것을 보호하기 위한 mutex라고 말할 수 있다.
  • 쉽게 말해, Python 프로그램이 Multi Thread를 사용할때, 실행중에 Interpreter를 Lock이 되는 상황이 있다는 말이다.
  1. 이게 무슨 말이지?
  • 멀티 스레드 환경에서 동시 접근을 Python 자체가 Lock한다는건가?
    • 맞다. 사용자의 코드로 발생하는 race condition이 아닌 Python 구현체인 Cpython 구현 자체에서 race condition이 발생 할 수 있다는 것이다.
  1. 그럼...왜 GIL이 있을까? 없으면 좋은것이 아닌가?
  • 성능 이슈에도 불구하고, Cpython이 GIL이라는 제약을 가져간 이유를 알기위해서는 Cpython이 메모리를 어떻게 관리하는지에 대한 이해가 필요하다.
  1. Cpython에서 메모리 관리하는 방법은?
  • Reference Counting을 이용한다.
  • 오브젝트마다 자신을 참조하고 있는 변수의 갯수를 저장하고, 이 수가 0이 되었을때, Garbage로 판단하고 메모리 해제를 진행하는것이다.
    • 그럼 GIL의 사용 이유가!!!! GC(Garbage Collection)를 처리하는 절차때문에 그렇구나!!!!
    • Multi Thread를 사용하는 상황에서 GC 수행시 문제가 생길수 있기 때문에 GIL을 사용하는것이다.
  1. Reference Counting GC는?
  • 장점 : 이 방식은 Garbage를 메모리에 쌓아놓지 않고, 곧 바로 해제 할 수 있다는 점에서 메모리 공간 측면에서 이점이 있다.
  • 단점 : 반면 참조가 해제되거나 추가될 때마다 실시간으로 Reference Count가 동기화 되어 야한다. Reference Count를 업데이트 하는 작업은 Atomic한 연산이 되어야 하는것이다. 그렇지 않다면 여러 스레드에서 동시에 접근해 원지 않는 결과가 나올수 있다.
    또한, Reference Counting은 순환참조하는 객체에 대해서는 탐지가 불가능하다. 그렇기 때문에 Python에는 Main으로 Reference Counting을 사용하고, 순환참조 탐지를 위한 GC를 보조적으로 사용한다.
  • 참고 : https://blog.winterjung.dev/2018/02/18/python-gc
    5-1 순환 참조는 어떻게 감지 하고 삭제 할까? (Cyclic GC 사용)
    • 순환 참조는 어떻게 생길까?
      • 간단한 예는 자기 자신을 참조하는 객체
      • 서로 참조하는 객체
        자신을 참조 하는 객체 예제,
        l = []        >> ref => 1
        l.append(l}   >> ref => 2
        del l         >> ref => 1 !!!
    • 먼저 순환 참조는 컨테이너 객체(e.g. tuple, list, set, dict, class)에 의해서만 발생할 수 있다.
    • 컨테이너 객체는 다른 객체에 대한 참조를 보유할 수 있어 정수, 문자열은 무시한 채 관심사를 컨테이너 객체에만 집중할 수 있다.
    • 순환 참조를 해결하기 위한 아이디어로 모든 컨테이너 객체를 추적한다. 여러 방법이 있겠지만 객체 내부의 링크 필드에 더블 링크드 리스트를 사용하는 방법이 가장 좋다. 이렇게 하면 추가적인 메모리 할당 없이도 컨테이너 객체 집합에서 객체를 빠르게 추가하고 제거할 수 있다. 컨테이너 객체가 생성될 때 이 집합에 추가되고 제거될 때 집합에서 삭제된다.
    • PyGC_Head에 선언된 더블 링크드 리스트를 사용한다.
      • 이제 모든 컨테이너 객체에 접근할 수 있으니 순환 참조를 찾을 수 있어야 한다. 순환 참조를 찾는 과정은 다음과 같다.
      1. 객체에 gc_refs 필드를 레퍼런스 카운트와 같게 설정한다.
      2. 각 객체에서 참조하고 있는 다른 컨테이너 객체를 찾고, 참조되는 컨테이너의 gc_refs를 감소시킨다.
      3. gc_refs가 0이면 그 객체는 컨테이너 집합 내부에서 자기들끼리 참조하고 있다는 뜻이다.
      4. 그 객체를 unreachable 하다고 표시한 뒤 메모리에서 해제한다.
  1. GIL을 사용하지 않고, Reference Counting을 Atomic을 보장할수 있을까?
  • 안됨. 매 연산마다 Lock을 걸어줘야 하는데, 이는 성능에 상당한 영향을 미치고, 최악의 경우 Dead Lock을 만들수도 있다. 그렇기 때문에 인터프리터 전체에 Lock을 걸어 Atomic을 보장한다.
  1. 그럼 이게 어떤영향을 주는것일까?
  • Cpython에서는 오직 하나의 스레드만이 인터프리터에 대한 점유권을 획득하기때문에, 멀티 스레드 환경에서 서로 Lock에 대하 점유권을 획득하는 과정에서 Bottleneck이 될수 있고, 성능도 느려질수 있다.
  • 단, I/O Bound Task에서는 GIL의 영향을 받지 않음
  1. 대안은?
  • CPU Bound Task에서 동시성/병렬성이 필요한 경우 Multi Processing을 하는 것 좋다.
  1. 왜 Reference Counting GC를 선택했을까?
  • Cpython의 구현언어인 C언어적인 부분이다. 원래 순환 개체에 대한 가비지 수집이 없었으므로 주기로 인해 메모리 누수가 발생했고, C API 및 데이터 구조는 참조 카운팅을 원칙을 기반한다. 실제 가비지 수집이 추가되었을때는 기존 바이너리 API와 이에 의존하는 모든 라이브러리를 중단할 수 없었기 때문에 Reference Counting을 유지 해야만했다...
  • 결국 C언어적인 측면으로 인하여 근간을 흔들수 없었던것 같다.
  1. Java와 비교하자!!! Java에는 GIL이 왜 없지?
  • Java는 Compile언어 이면서 JVM에 수행되는 Interepreter 언어 이다.
  • Java는 Mark and Sweep(GC)를 사용하여 메모리를 관리하며, 참조 갯수를 저장하지 않는다.
  • root부터 하나씩 참조를 따라가면서 찾는 객체가 살아 있음을 Marking한 후 전체 Heap 객체들을 순횐하면서 Marking되지 않은 객체들을 제거하는 방식이다.
  • Mark and Sweep에서는 메모리가 일정 수준 이상 찼을때, 위에 말한 Collecting 작업이 시작된다.
  • 그렇기 때문에 오브젝트 참조가 변경 될때마다 Atomic한 연산이 수행 될 필요는 없고 GIL이 필요가 없다. 대신, 참조를 확인하는 Marking과정에서 모든 스레드가 일시적으로 중단하여 GC의 Atomic을 보장하는데, 이 시기를 Stop the world라고 하고, 이 Stop the world를 줄이기 위한 여러 GC 알고리즘이 발전하고 있는것이다.
  • 단점 : Mark and Sweep방법은 일반적인 상황에서 스레드 동시접근이 가능한 장점이 있으나, Garbage를 곧바로 회수하는 것이 아닌 메모리에 쌓아 놓기 때문에 메모리 공간 측면에서는 단점이다. 그리고 객체의 소멸 시점을 예측하기가 힘들다. 즉!! 소멸시점이 예측되어야하거나 메모리 공간 사용의 제약이 있는경우 Reference Counting 방법이 유리할 수 있다.

https://velog.io/@litien/GIL-Java%EC%97%90%EB%8A%94-%EC%97%86%EB%8D%98%EB%8D%B0

profile
Fullstack developer

1개의 댓글

comment-user-thumbnail
2023년 12월 20일

너무 좋은 글 감사합니다. 글의 흐름이 너무 좋아요

답글 달기