가. GIL in CPython
1) GIL이란
•
GIL(Global Interpreter Lock): 멀티 쓰레드 환경에서 하나의 쓰레드만 인터프리터의 자원을 독점 사용하는 제약
→ GIL은 자원점유 측면에서 OS의 mutex(or lock)의 기능과 유사하다
•
GIL은 C 언어로 개발된 Cpython 구현체의 인터프리터에만 존재함
→ C 외 언어로 구현된 Python 구현체의 인터프러터에는 GIL 방식을 채택하지 않음
•
용어정리: CPU bound, I/O bound, CPU burst, I/O burst
→ CPU bound: 프로그램 성능이 CPU burst에 의해 묶인(제한받는) 것
→ I/O bound: 프로그램 성능이 I/O burst에 의해 묶인(제한받는) 것
→ CPU burst: cpu 연산이 연속되는 것
→ I/O burst: 네트워크나 Disk Drive에서 I/O waiting이 연속되는 것
→ burst: 한 번에 연산(또는 전송)되는 데이터 블럭
2) GIL 도입 이유
•
CPython의 메모리 관리방식(reference counting)이 thread-safe하지 않기 때문에 이를 보완하기 위한 방법으로 GIL이 도입되었음
→ 멀티 쓰레드 환경에서 참조 카운팅(reference counting)의 메모리 관리 방식에 따른 부작용을 제거하기 위한 솔루션
→ 예를 들어, a라는 객체 인스턴스를 참조한 경우가 2번이라면 참조 카운트는 2가 됨. 두 개의 쓰레드가 동시에 관련 참조를 해제하여 다른 쓰레드의 참조해제를 카운트에 반영하지 못하여 참조 카운트가 1이 된다면 객체 참조와 관련된 다양한 이슈가 발생할 수 있음.
3) GIL context switching
•
in common: 특정 쓰레드의 I/O 작업이 끝이나면 GIL을 다른 쓰레드로 넘겨줌
•
in Python2 way: 일련의 코드 실행 후 GIL을 넘겨주는 방식 추가
→ 추가 설명: 100 byte-code의 instruction 후에 다른 쓰레드에게 GIL을 점유할 기회 전달
→ 배경: CPU Bound 작업의 경우 I/O 작업이 거의 없기 때문에 특정 쓰레드가 매우 긴 시간 동안 대기상태로 남겨지는 경우에 대한 솔루션 필요
•
in Python3 way: 시간을 주기로 GIL을 다른 쓰레드로 넘겨주는 방식 추가
→ 추가 설명: 5ms를 기준으로 다른 쓰레드에게 GIL을 점유할 기회 전달
→ 배경: byte-code instruction에 따라 실행 시간이 매우 다르기 때문에 시간적인 면에서 비효과적으로 GIL 점유하게 됨
3) GIL 성능 이슈
•
CPU Bound Process 성능 이슈
→ Multi CPU core 환경에서 여러 프로세스가 존재하지만 오직 하나의 쓰레드에게만 자원 점유를 허용함
5) GIL 성능 이슈 극복 방법
•
CPU Bound Process 성능 이슈
→ MultiProcessing 라이브러리 활용
→ Celery와 브로커를 조합한 분산 비동기 기반의 메시지 큐 방식 활용
→ CPython 외의 Python 구현체를 사용하는 인터프리터 사용
참고) CPython 계열의 라이브러리 사용 불가(에를 들어, Numpy 등)
4) GIL 유지하는 이유
•
CPython 기반 API가 reference counting 방식에 의존되어 있으므로 GIL을 제거하는데 어려움이 있음
→ CPython은 C 언어의 API에 의존하고 있음. 해당 API의 주요 로직은 reference counting 방식 설계 원칙에 의존되어 있음.
•
싱글쓰레드 기반의 작업에 있어, GIL 제거 후 보다 제거 전의 성능이 뛰어남
•
멀티쓰레드 기반으로 작업 시 쓰레드 간 자원공유 실패를 걱정하지 않아도 됨
6) AI 라이브러리는 GIL에 영향을 받지 않나?
•
검증 필요
◦
GIL은 CPU 자원 스위칭에 이슈가 있지만 GPU에는 관련 이슈가 없음
◦
AI 라이브러리는 주로 GPU 자원에 의존한 작업이므로 GIL 이슈는 큰 제약사항이 되지 않음
나. Mark and Sweep vs Reference Counting
1) Mark and Sweep
•
동작원리: 루트 객체를 시작으로 참조하고 있는 모든 객체를 순회하며 표시(Marking), 힙 영역의 객체 중 Marking 되어 있지 않은 객체는 제거(Sweep)
→ Marking하는 시점에서 모든 쓰레드의 동작을 중지시킴(it is called ‘stop the world’)
•
장점: 쓰레드의 동시접근을 허용하므로 멀티쓰레딩 동작에 따른 성능 향상 가능
•
단점: 객체 제거 전까지 불필요한 객체가 메모리에 남아있기 때문에 메모리 낭비 이슈 존재
•
Mark and Sweep in Python: 메모리 관리 방식에서 보조로 사용함. 주 방식은 Reference Counting
•
Mark and Sweep in Java, Node: 메모리 관리 방식에서 주 수단으로 사용함
2) Reference Counting
•
동작원리: 객체별로 참조에 횟수를 저장함, 참조횟수가 0이된다면 메모리에서 제거하는 방식
•
장점: 불필요 객체가 즉시 제거됨으로써 메모리를 효율적으로 사용할 수 있음
→ 메모리에 큰 제약사항이 있는 경우 유리
•
단점: 한 시점에 오직 하나의 쓰레드만 자원을 점유할 수 있으므로 멀티쓰레딩 환경에서 성능 이슈 발생
3) Python 메모리 관리 방식
•
Python은 주로 Reference Counting 방식으로 메모리를 관리하지만 Mark and Sweep 방식을 보조 방식으로 사용함
•
순환참조의 경우, Reference Counting 방식으로 메모리 누수를 방지할 수 없으므로 Mark and Sweep 방식으로 보조가 필요함
→ 순환참조가 발생할 수 있는 경우: 자기 자신 참조, 서로 참조 등이 있음
# 자기 참조
>>> l = []
>>> l.append(l)
>>> del l
# 서로 참조
>>> a = Foo() # 0x60
>>> b = Foo() # 0xa8
>>> a.x = b # 0x60의 x는 0xa8를 가리킨다.
>>> b.x = a # 0xa8의 x는 0x60를 가리킨다.
# 이 시점에서 0x60의 레퍼런스 카운터는 a와 b.x로 2
# 0xa8의 레퍼런스 카운터는 b와 a.x로 2다.
>>> del a # 0x60은 1로 감소한다. 0xa8은 b와 0x60.x로 2다.
>>> del b # 0xa8도 1로 감소한다.
Python
복사
code from https://blog.winterjung.dev/2018/02/18/python-gc
4) 객체의 메모리 효율적 관리 방법
•
slot 활용에 따른 메모리 절약: 인스턴스 객체의 속성의 키 값만 list로 관리하여 메모리 효율을 높일 수 있음
→ 인스턴스 객체의 속성값은 기본적으로 키와 값을 모두 포함한 dict로 관리됨
→ 속성값의 키만을 list로 관리하면 메모리를 절약할 수 있음
→ 리팩토링 시 객체에 대해 __dict__ 메소드를 활용이 필요하지 않다면 dict 대신 slot의 형태로 관리하는 방법 권장
•
slot 적용: 클래스 객체의 상단에 __slots__ 정의
class Human:
__slots__ = ["name", "weight"]
def __init__(self, name, weight):
self.name = name
self.weight = weight
mj = Human("MJ", 12)
print(mj.__slots__) # ['name', 'weight']
print(mj.__dict__) # AttributeError: 'Human' object has no attribute '__dict__'
Python
복사
Reference
•
Java vs Python, https://velog.io/@litien/GIL-Java에는-없던데
•
GIL context switching 동작 방식, https://www.datacamp.com/community/tutorials/python-global-interpreter-lock
•
python standard I/O vs 비동기 I/O, https://towardsdatascience.com/why-you-should-use-async-in-python-6ab53740077e
•
python I/O bound 성능 이슈, https://towardsdatascience.com/why-you-should-use-async-in-python-6ab53740077e