1P by GN⁺ | ★ favorite | 댓글 1개
  • 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=1GIL 활성화를 강제하도록 설정 처리도 수정됨
  • 후속 작업은 호환되지 않는 확장을 로드할 때 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 PR
    • x86-64 MacOS Intel NoGIL PR
    • ARM64 MacOS M1 Refleaks NoGIL PR
    • ARM64 MacOS M1 NoGIL PR
    • AMD64 Ubuntu NoGIL Refleaks PR
    • AMD64 Ubuntu NoGIL PR
    • AMD64 Windows Server 2022 NoGIL PR

리뷰 중 추가된 범위

  • corona10Lib/test/test_cmd_line.py에 환경 변수 테스트를 추가할 가치가 있다고 제안함
  • 이후 다음 커밋들이 추가됨
    • Add test for PYTHON_GIL in test_cmd_line
    • Set enable_gil properly when PYTHON_GIL=1
    • Don't add 'enable_gil' to test_embed in normal builds
  • colesbury는 환경 변수를 추가하는 시점에 문서화하는 것이 좋다고 봄
    • 이미 --disable-gil configure 플래그는 문서화되어 있다는 점을 근거로 듦
    • 문서에는 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 option
    • Fix link to -X gil
    • Fix PYTHON_GIL versionchanged line
    • Clarify test_flags in normal builds
  • ericsnowcurrently, erlend-aasland, corona10, colesbury가 변경을 승인함
  • 병합 커밋은 2731913이며, 병합 뒤 vstinner는 이 변경을 “흥미롭고 매우 무섭다”고 반응함

후속 작업

  • 후속 이슈로 두 가지 작업이 분리됨
    • #116322: 호환되지 않는 확장을 로드할 때 GIL을 다시 활성화하는 작업
    • #116329: GIL을 기본적으로 비활성화하는 작업
  • 현재 PR은 GIL 기본값 변경이 아니라, free-threaded 빌드에서 사용자가 환경 변수나 -X 옵션으로 GIL 상태를 제어할 수 있게 하는 변경임

댓글과 토론

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

  • 기본 Python을 얼마나 더 빠르게 만들 수 있을지 기대됨. 그 문제를 완화하려는 도구가 너무 많아지면서 Python의 가치 제안도 도전받고 있음
    속도 개선 도구로는 Mojo, pytorch, triton, numba, taichi가 떠오름. 이 문제를 풀려는 시도가 너무 많아서, 지난번 하나를 써보려 했을 때 선택지가 너무 많아 압도됐음. 결국 taichi를 골랐고 꽤 재미있고 쓰기 쉬웠지만, 적용 범위는 다소 제한적이었음

  • https://peps.python.org/pep-0703/에 설명된 편향 참조 카운팅 방식이 왜 단일 스레드 친화성만 두고, 다른 스레드에서 접근하면 원자적 증가/감소를 요구하는지 궁금함
    다른 구현들, 예를 들어 편향 참조 카운팅을 구현한 여러 Rust 크레이트에서는 새 스레드로 옮길 때만 원자적으로 증가시키고, 그 스레드는 다시 0에 도달할 때까지 비원자적 증가/감소를 하다가 마지막에 원자적 감소를 수행하는 방식을 봤음. 기존 시스템에 덧붙이는 형태라 단일 PyObject가 있고, 새 스레드 로컬 객체를 가리키도록 교체할 수 없기 때문인지 궁금함

    • 앞으로 CPython에서 소유권 이전을 구현할 수도 있지만, 조금 더 까다로움
      Rust에서는 소유권 이전을 위한 "move"가 언어의 일부지만, C나 Python에는 그에 대응하는 개념이 없어서 언제 소유권을 이전해야 하는지, 어떤 스레드가 새 소유자가 되어야 하는지 판단하기 어려움. 휴리스틱을 쓸 수는 있음. 예를 들어 객체를 queue.SimpleQueue에 넣을 때 소유권을 포기하거나 이전할 수 있겠지만, 그 경우에도 큐에 들어간 객체를 어떤 스레드가 "get"할지 미리 알기 어려움
      성능상 이득도 작을 것 같음. 많은 객체는 단일 스레드에서만 접근되고, 일부 객체는 여러 스레드에서 접근되지만, 한 스레드에서만 독점적으로 접근되다가 이후 다른 스레드에서만 독점적으로 접근되는 객체는 드묾
  • 먼저 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은 Global Interpreter Lock의 약자임

  • 여기서 더 큰 그림을 잘 정리한 자료가 있을까?

  • 드디어 여러 도구의 벤치마크가 기대됨