gh-116167: GIL 비활성화 허용
(github.com/python)- CPython PR #116338은 free-threaded 빌드에서
PYTHON_GIL=0또는-X gil=0으로 GIL을 비활성화할 수 있게 하는 변경을python:main에 병합함 - 런타임에서 GIL을 다시 켤 수 있는 가능성을 남기기 위해 GIL 관련 자료구조는 평소처럼 초기화하고, 비활성화는 시작 시 플래그를 설정해
take_gil()과drop_gil()이 일찍 반환하도록 처리함 - 초기 확인에서는
PYTHON_GIL=0설정으로 스레드를 쓰지 않는 일부 테스트와 작은 프로그램은 정상 동작했고, 매우 기본적인 스레드 프로그램은 때때로 동작했지만 전체 테스트 스위트는test_asyncio에서 빠르게 크래시함 - 리뷰 과정에서
PYTHON_GIL테스트, 문서화,-X gil옵션,sys.flags반영이 추가됐고,PYTHON_GIL=1이 GIL 활성화를 강제하도록 설정 처리도 수정됨 - 후속 작업은 호환되지 않는 확장을 로드할 때 GIL을 다시 켜는 문제와 GIL을 기본 비활성화하는 문제로 분리됐으며, 이 변경은 Python 3.13의 free-threaded 빌드에서 GIL 제어 표면을 추가함
병합된 변경
- CPython PR #116338은
gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0변경을 다룸 colesbury가 2024년 3월 11일python:main에 병합함- 변경 규모는 12개 파일, 163줄 추가, 1줄 삭제로 표시됨
- 대상 기능은 일반 빌드가 아니라 free-threaded 빌드에서 GIL을 비활성화하는 실행 옵션임
GIL 비활성화 방식
- free-threaded 빌드에서 다음 설정으로 GIL을 비활성화할 수 있음
PYTHON_GIL=0-X gil=0
- 런타임에서 GIL을 다시 켤 수 있도록, 모든 GIL 관련 자료구조는 평소처럼 초기화됨
- 실제 비활성화는 시작 시 플래그를 설정하는 방식임
- 이 플래그 때문에
take_gil()과drop_gil()이 일찍 반환함
- 이 플래그 때문에
- 리뷰 중
PYTHON_GIL=1일 때enable_gil을 올바르게 설정하는 커밋도 추가됨
테스트와 현재 제약
PYTHON_GIL=0설정으로 일부 테스트와 작은 프로그램을 점검함- 스레드를 사용하지 않는 테스트와 작은 프로그램은 정상 동작하는 것으로 확인됨
- 매우 기본적인 스레드 프로그램은 때때로 동작함
- 전체 테스트 스위트는 빠르게 크래시했으며, 위치는
test_asyncio로 기록됨 !buildbot nogil명령으로 NoGIL 관련 빌더 테스트가 여러 차례 예약됨x86-64 MacOS Intel ASAN NoGIL PRx86-64 MacOS Intel NoGIL PRARM64 MacOS M1 Refleaks NoGIL PRARM64 MacOS M1 NoGIL PRAMD64 Ubuntu NoGIL Refleaks PRAMD64 Ubuntu NoGIL PRAMD64 Windows Server 2022 NoGIL PR
리뷰 중 추가된 범위
corona10은Lib/test/test_cmd_line.py에 환경 변수 테스트를 추가할 가치가 있다고 제안함- 이후 다음 커밋들이 추가됨
Add test for PYTHON_GIL in test_cmd_lineSet enable_gil properly when PYTHON_GIL=1Don't add 'enable_gil' to test_embed in normal builds
colesbury는 환경 변수를 추가하는 시점에 문서화하는 것이 좋다고 봄- 이미
--disable-gilconfigure 플래그는 문서화되어 있다는 점을 근거로 듦 - 문서에는 free-threaded 빌드에서만 사용 가능하다는 점,
0은 GIL 비활성화 강제,1은 GIL 활성화 강제, Python 3.13 신규 사항을 포함해야 한다고 정리함
- 이미
- 이후
Document PYTHON_GIL environment variable커밋이 추가됨
-X gil 옵션 추가와 최종 병합
- Discord 논의 이후 환경 변수와 함께 사용할
-X옵션도 추가하기로 함 - PR 제목은
PYTHON_GIL=0만 다루던 형태에서PYTHON_GIL=0 or -X gil=0까지 포함하도록 변경됨 - 추가 커밋에는 다음 내용이 포함됨
Add -X gil option, add to sys.flags, modify test to cover env var… and optionFix link to -X gilFix PYTHON_GIL versionchanged lineClarify test_flags in normal builds
ericsnowcurrently,erlend-aasland,corona10,colesbury가 변경을 승인함- 병합 커밋은
2731913이며, 병합 뒤vstinner는 이 변경을 “흥미롭고 매우 무섭다”고 반응함
후속 작업
댓글과 토론
Hacker News 의견들
-
no-GIL 작업이 궁금한 사람을 위해 추가 링크를 남김: [0], [1]
[0] Multithreaded Python without the GIL
https://docs.google.com/document/d/18CXhDb1ygxg-YXNBJNzfzZsD...
[1] Github repo
https://github.com/colesbury/nogil- noGIL 전반에 대한 추가 맥락은 여기서 볼 수 있음: https://hn.algolia.com/?dateRange=all&page=0&prefix=true&que...
- 위 링크들은 둘 다 꽤 오래됨. 더 최신 내용은 PEP 703과 Sam의 nogil-3.12 저장소를 보는 게 좋음
[0] https://peps.python.org/pep-0703/
[1] https://github.com/colesbury/nogil-3.12
-
기본 Python을 얼마나 더 빠르게 만들 수 있을지 기대됨. 그 문제를 완화하려는 도구가 너무 많아지면서 Python의 가치 제안도 도전받고 있음
속도 개선 도구로는 Mojo, pytorch, triton, numba, taichi가 떠오름. 이 문제를 풀려는 시도가 너무 많아서, 지난번 하나를 써보려 했을 때 선택지가 너무 많아 압도됐음. 결국 taichi를 골랐고 꽤 재미있고 쓰기 쉬웠지만, 적용 범위는 다소 제한적이었음- Mojo는 Python의 상위 집합이라는 점 때문에 Python 생태계에 대한 공격으로 봐야 함. Python을 사용할 수는 있지만, 그 자체가 Python은 아님
Taichi는 정말 저평가되어 있음. Metal을 포함해 모든 플랫폼에서 동작하고, 예제가 많으며, 코드 작성도 쉬움. 무엇보다 생태계와 통합되며 기존 생태계를 대체하지 않음
https://github.com/taichi-dev
Taichi로 무엇을 할 수 있는지 보여주는 훌륭한 데모 영상: https://www.youtube.com/watch?v=oXRJoQGCYFg
https://www.youtube.com/watch?v=WNh4Q7-OSJs
https://www.taichi-lang.org/
- Mojo는 Python의 상위 집합이라는 점 때문에 Python 생태계에 대한 공격으로 봐야 함. Python을 사용할 수는 있지만, 그 자체가 Python은 아님
-
https://peps.python.org/pep-0703/에 설명된 편향 참조 카운팅 방식이 왜 단일 스레드 친화성만 두고, 다른 스레드에서 접근하면 원자적 증가/감소를 요구하는지 궁금함
다른 구현들, 예를 들어 편향 참조 카운팅을 구현한 여러 Rust 크레이트에서는 새 스레드로 옮길 때만 원자적으로 증가시키고, 그 스레드는 다시 0에 도달할 때까지 비원자적 증가/감소를 하다가 마지막에 원자적 감소를 수행하는 방식을 봤음. 기존 시스템에 덧붙이는 형태라 단일 PyObject가 있고, 새 스레드 로컬 객체를 가리키도록 교체할 수 없기 때문인지 궁금함- 앞으로 CPython에서 소유권 이전을 구현할 수도 있지만, 조금 더 까다로움
Rust에서는 소유권 이전을 위한 "move"가 언어의 일부지만, C나 Python에는 그에 대응하는 개념이 없어서 언제 소유권을 이전해야 하는지, 어떤 스레드가 새 소유자가 되어야 하는지 판단하기 어려움. 휴리스틱을 쓸 수는 있음. 예를 들어 객체를 queue.SimpleQueue에 넣을 때 소유권을 포기하거나 이전할 수 있겠지만, 그 경우에도 큐에 들어간 객체를 어떤 스레드가 "get"할지 미리 알기 어려움
성능상 이득도 작을 것 같음. 많은 객체는 단일 스레드에서만 접근되고, 일부 객체는 여러 스레드에서 접근되지만, 한 스레드에서만 독점적으로 접근되다가 이후 다른 스레드에서만 독점적으로 접근되는 객체는 드묾
- 앞으로 CPython에서 소유권 이전을 구현할 수도 있지만, 조금 더 까다로움
-
먼저 tranched bread 소식을 읽었는데, 이제 이것까지? 대단한 시대임
Unladen Swallow 프로젝트 [1]가 흐지부지됐을 때는 좀 아쉬웠음. Python이 다시 핵심 최적화 경로로 돌아오는 걸 보니 좋음
[1] https://en.wikipedia.org/wiki/CPython#Unladen_Swallow -
다섯 살에게 설명하듯 알려줬으면 함
GIL이 무엇인지는 개념적으로 알겠음. 그런데 이 변경의 영향은 무엇임? 전반적인 성능 향상을 기대하면서 이제 패키지들이 깨지게 되는 건가?- 예전에는 GIL 때문에 사실상 다중 스레드 Python을 거의 작성하지 않았음. 스레드는 주로 독립적인 입출력에서 막힐 수 있는 여러 작업을 처리할 때 쓰였고, 물론 흔하고 유용하지만 CPU 중심 Python 코드의 성능에는 도움이 되지 않았음
고강도 CPU 작업이 아니어도 이 변화는 유용할 수 있음. 최근에는 많은 코드가 Python의 네이티브 asyncio 언어 기능으로 작성됨. 이는 NodeJS처럼 async/await로 실행을 양보하며 단일 스레드에서 동작하고, 단일 스레드만으로도 초당 수천 요청 수준의 꽤 좋은 처리량을 낼 수 있음
하지만 큰 문제는 어떤 CPU 작업이든 수행하는 순간 다른 모든 코루틴을 막아버려서, 온갖 모호한 문제가 생기고 초당 요청 수가 망가진다는 점임. 예를 들어 한 코루틴에서 무작위 입출력 타임아웃이 보이는데, 실제 원인은 전혀 다른 코루틴이 잠시 CPU를 점유했기 때문일 수 있음. 왜 이런 일이 생기는지 관측하기도 매우 어려움. asyncio는 블로킹 작업을 메인 스레드 밖으로 빼는 데 도움이 되는asyncio.to_thread()함수 [1]를 제공하지만, GIL 때문에 CPU 중심 작업이 다른 코루틴에 간섭하지 않게 진짜로 분리해주지는 못함
[1] https://docs.python.org/3/library/asyncio-task.html#asyncio.... - 어떤 패키지가 GIL에 의존한다면 GIL이 활성화됨. 패키지가 깨지지는 않음
- 예전에는 GIL 때문에 사실상 다중 스레드 Python을 거의 작성하지 않았음. 스레드는 주로 독립적인 입출력에서 막힐 수 있는 여러 작업을 처리할 때 쓰였고, 물론 흔하고 유용하지만 CPU 중심 Python 코드의 성능에는 도움이 되지 않았음
-
궁금한 사람을 위해, GIL은 Global Interpreter Lock의 약자임
-
여기서 더 큰 그림을 잘 정리한 자료가 있을까?
-
드디어 여러 도구의 벤치마크가 기대됨
- PEP-703은 2023년 6월에 NoGIL로 실행할 때 15% 오버헤드를 예측했음: https://discuss.python.org/t/pep-703-making-the-global-inter...