[Deferred] PEP 556 - Threaded garbage collection

원문 링크: PEP 556 - Threaded garbage collection

상태: Deferred 유형: Standards Track 작성일: 08-Sep-2017

PEP 556 – Threaded garbage collection

  • 작성자: Antoine Pitrou
  • 상태: 연기됨 (Deferred)
  • 유형: 표준 트랙 (Standards Track)
  • 생성일: 2017년 9월 8일
  • Python 버전: 3.7

연기 공지 (Deferral Notice)

이 PEP는 현재 활발히 작업 중이지 않습니다. 미래에 다시 논의될 수 있습니다. 주요 미완료 단계는 다음과 같습니다:

  • 구현을 다듬고, 필요한 경우 테스트 스위트를 조정합니다.
  • 스레드 방식의 가비지 컬렉션(GC) 설정이 기존 코드에 예상치 못한 방식으로 문제를 일으키지 않는지 확인합니다 (예상되는 영향으로는 참조 사이클(reference cycle)에 있는 객체의 수명 연장이 포함됩니다).

개요 (Abstract)

이 PEP는 CPython의 순환 가비지 컬렉터(cyclic garbage collector, GC)를 위한 새로운 선택적 동작 모드를 제안합니다. 이 모드에서는 암묵적인(즉, 기회주의적인) 컬렉션이 동기적으로(synchronously) 실행되는 대신 전용 스레드에서 발생합니다.

용어 (Terminology)

  • 암묵적 GC 실행 (Implicit GC run / Implicit collection): 새로운 할당이 요청될 때마다 할당 통계를 기반으로 계산된 특정 휴리스틱(heuristic)에 따라 기회주의적으로 트리거되는 GC 실행을 의미합니다. 이 휴리스틱의 세부 사항은 이 PEP의 변경 제안 범위에 포함되지 않습니다.
  • 명시적 GC 실행 (Explicit GC run / Explicit collection): gc.collect()와 같은 API 호출을 통해 프로그램적으로 요청되는 GC 실행을 의미합니다.
  • 스레드 방식 (Threaded): GC 실행이 애플리케이션 코드의 순차적 실행과 별개로 전용 스레드에서 발생한다는 사실을 나타냅니다. 이는 “동시적(concurrent)”을 의미하지 않습니다 (GIL(Global Interpreter Lock)은 여전히 전용 GC 스레드를 포함한 Python 스레드 간의 실행을 직렬화합니다). 또한 “병렬적(parallel)”을 의미하지도 않습니다 (GC는 GC 실행의 wall-clock latency를 줄이기 위해 작업을 여러 스레드에 동시에 분산할 수 없습니다).

배경 (Rationale)

GC의 동작 모드는 항상 암묵적 컬렉션을 동기적으로 수행하는 것이었습니다. 즉, 앞서 언급된 휴리스틱이 활성화될 때마다 현재 스레드의 애플리케이션 코드 실행이 중단되고, 죽은 참조 사이클(dead reference cycles)을 회수하기 위해 GC가 시작됩니다.

문제는 GC가 죽은 참조 사이클과 그에 딸린 보조 객체를 회수하는 과정에서 __del__ 메서드 및 weakref 콜백과 같은 임의의 파이널라이제이션(finalization) 코드를 실행할 수 있다는 점입니다. Python은 점점 더 정교한 목적으로 사용되고 있으며, 분산 시스템에서 객체 손실이 다른 (논리적 또는 물리적) 노드에 통지를 요구하는 등 파이널라이제이션 코드가 복잡한 작업을 수행하는 경우가 점점 흔해지고 있습니다.

일관된 내부 상태 및/또는 동기화 프리미티브(synchronization primitives) 획득에 의존할 수 있는 파이널라이제이션 코드를 실행하기 위해 임의의 지점에서 애플리케이션 코드를 중단하는 것은, 가장 노련한 전문가조차 제대로 해결하기 어려운 재진입(reentrancy) 문제를 야기합니다.

이 PEP는 겉으로는 비슷해 보이지만, 동일 스레드 내 재진입(same-thread reentrancy)이 다중 스레드 동기화(multi-thread synchronization)보다 근본적으로 더 어려운 문제라는 관찰에 기반합니다. 각 개발자나 라이브러리 작성자가 개별적으로 극도로 어려운 재진입 문제로 씨름하게 하는 대신, 이 PEP는 잘 알려진 다중 스레드 동기화 관행만으로 충분한 별도의 스레드에서 GC가 실행되도록 허용하는 것을 제안합니다.

제안 (Proposal)

이 PEP에 따라 GC는 두 가지 동작 모드를 가집니다:

  • “serial” (직렬): 기본이자 레거시(legacy) 모드입니다. 암묵적 GC 실행이 (앞서 언급된 할당 휴리스틱에 따라) 필요하다고 감지하는 스레드에서 즉시 수행됩니다.
  • “threaded” (스레드 방식): 런타임에 프로세스별로 명시적으로 활성화될 수 있습니다. 암묵적 GC 실행은 할당 휴리스틱이 트리거될 때마다 예약되지만, 전용 백그라운드 스레드에서 실행됩니다.

“serial” 모드에서 파이널라이제이션 콜백의 정교한 사용을 괴롭히던 어려운 재진입 문제는 “threaded” 모드에서는 비교적 쉬운 다중 스레드 동기화 문제가 됩니다.

GC는 전통적으로 gc.collect Python API 및 PyGC_Collect C API를 사용하여 명시적 GC 실행도 허용합니다. 이 두 API의 가시적 의미는 변경되지 않습니다. 호출될 때 즉시 GC 실행을 수행하며, GC 실행이 완료될 때만 반환됩니다.

새로운 공개 API (New public APIs)

gc 모듈에 두 가지 새로운 Python API가 추가됩니다:

  • gc.set_mode(mode): 현재 동작 모드를 설정합니다 (“serial” 또는 “threaded”). “serial”로 설정하고 현재 모드가 “threaded”인 경우, 이 함수는 GC 스레드가 종료될 때까지 기다립니다.
  • gc.get_mode(): 현재 동작 모드를 반환합니다.

동작 모드를 자유롭게 전환하는 것이 허용됩니다.

의도된 사용 (Intended use)

이 전환이 프로세스 단위(per-process)로 적용되며 모든 파이널라이제이션 콜백의 의미론에 영향을 미치므로, 애플리케이션 코드 시작 부분(및/또는 multiprocessing을 사용하는 경우 자식 프로세스의 초기화 함수에서)에 설정하는 것이 권장됩니다. 라이브러리 함수는 gc.enable 또는 gc.disable을 호출하지 않아야 하는 것처럼 이 설정을 변경하지 않는 것이 좋지만, 그렇게 하는 것을 막지는 않습니다.

비목표 (Non-goals)

이 PEP는 다른 종류의 비동기 코드 실행(예: signal 모듈에 등록된 시그널 핸들러)과 관련된 재진입 문제를 다루지 않습니다. 작성자는 고통스러운 재진입 문제의 압도적인 대다수가 파이널라이저(finalizers)와 함께 발생한다고 믿습니다. 대부분의 경우 시그널 핸들러는 단일 플래그를 설정하거나 메인 프로그램이 알아차릴 수 있도록 파일 디스크립터(file descriptor)를 깨울 수 있습니다. 예외를 발생시키는 시그널 핸들러의 경우, 스레드 내에서 실행되어야 합니다.

이 PEP는 또한 일반적인 참조 카운팅(reference counting)의 일부로 호출될 때, 즉 가시적 참조가 해제되어 객체의 참조 카운트가 0이 될 때 발생하는 파이널라이제이션 콜백의 실행을 변경하지 않습니다. 이러한 실행은 코드의 결정론적인 지점에서 발생하므로 일반적으로 문제가 되지 않습니다.

논의 (Discussion)

기본 모드 (Default mode)

기본 모드를 단순히 “threaded”로 변경해야 하는지에 대한 의문이 있을 수 있습니다. 다중 스레드 애플리케이션의 경우 문제가 되지 않을 것입니다. 이러한 애플리케이션은 이미 임의의 스레드에서 파이널라이제이션 핸들러가 실행될 준비가 되어 있어야 합니다. 그러나 단일 스레드 애플리케이션에서는 현재 파이널라이저가 항상 메인 스레드에서 호출된다는 것이 보장됩니다. 이 속성을 깨뜨리는 것은 미묘한 동작 변경이나 버그를 유발할 수 있으며, 예를 들어 파이널라이저가 일부 스레드 로컬 값에 의존하는 경우에 그렇습니다.

또 다른 문제는 프로그램이 동시성을 위해 fork()를 사용할 때 발생합니다. 단일 스레드 프로그램에서 fork()를 호출하는 것은 안전하지만, 프로그램이 다중 스레드인 경우에는 (적어도) 불안정합니다.

명시적 컬렉션 (Explicit collections)

명시적 컬렉션도 백그라운드 스레드에 위임해야 하는지 물을 수 있습니다. 대답은 별로 중요하지 않다는 것입니다. gc.collectPyGC_Collect는 실제로 컬렉션이 끝날 때까지 기다리므로 (이 속성을 깨뜨리면 호환성이 깨짐), 실제 작업을 백그라운드 스레드에 위임해도 명시적 컬렉션을 요청하는 스레드와의 동기화를 쉽게 할 수는 없습니다.

결론적으로, 이 PEP는 위 의사 코드에 기반하여 구현하기에 더 간단해 보이는 동작을 선택합니다.

메모리 사용량에 미치는 영향 (Impact on memory use)

“threaded” 모드는 기본 “serial” 모드에 비해 암묵적 컬렉션에 약간의 지연을 초래합니다. 이는 특정 애플리케이션의 메모리 프로파일을 변경할 수 있습니다. 실제 사용 시 얼마나 많은지는 측정해야 하지만, 영향이 미미하고 견딜 만할 것으로 예상합니다. 첫째, 암묵적 컬렉션은 효과가 결정론적인 가시적 동작으로 이어지지 않는 휴리스틱에 기반하기 때문입니다. 둘째, GC는 참조 사이클을 처리하는 반면, 많은 객체는 마지막 가시적 참조가 사라질 때 즉시 회수되기 때문입니다.

CPU 소비량에 미치는 영향 (Impact on CPU consumption)

위 의사 코드에 따르면 “threaded” 모드에서 각 암묵적 컬렉션 요청에 대해 두 번의 잠금(lock) 작업이 추가됩니다. 하나는 요청을 하는 스레드에서(release 호출), 다른 하나는 GC 스레드에서(acquire 호출) 발생합니다. 또한, 현재 모드와 관계없이 각 실제 컬렉션 주변에 두 번의 잠금 작업이 더 추가됩니다.

현대 시스템에서 이러한 잠금 작업의 비용은 컬렉션 자체 동안 포인터 체인을 크롤링하는 실제 비용(“pointer chasing”은 예측 및 슈퍼스칼라 실행에 적합하지 않아 현대 CPU에서 가장 어려운 작업 부하 중 하나)에 비해 매우 작을 것으로 예상됩니다.

최악의 경우 미니 벤치마크에 대한 실제 측정은 안심할 수 있는 상한선을 제공하는 데 도움이 될 수 있습니다.

GC 일시 정지에 미치는 영향 (Impact on GC pauses)

이 PEP는 GC 일시 정지(GC pauses) 자체에 직접적으로 초점을 맞추지 않지만, 암묵적 컬렉션 중 어느 시점에서 GIL을 해제하는 것이 (예를 들어 순수 Python 파이널라이저를 실행함으로써) 그 사이에 애플리케이션 코드가 실행되도록 허용하여 일부 애플리케이션에서 가시적인 GC 일시 정지 시간을 줄일 수 있는 실질적인 가능성이 있습니다.

이 PEP가 수락된다면, 미래 작업에서는 컬렉션 중에 GIL을 추측적으로 해제하여 이 잠재력을 더 잘 실현하려고 시도할 수 있지만, 이것이 얼마나 실현 가능한지는 불분명합니다.

미해결 문제 (Open issues)

  • gc.set_mode는 여러 동시 호출로부터 보호되어야 합니다. 또한 GC 실행 중 (즉, 파이널라이저에서) 호출될 때 예외를 발생시켜야 합니다.
  • 종료 시에는 어떻게 될까요? _PyGC_Fini()가 호출될 때까지 GC 스레드가 실행될까요?

구현 (Implementation)

작성자의 GitHub 포크의 threaded_gc 브랜치에 초안 구현이 제공됩니다.

참고 자료 (References)

https://peps.python.org/pep-0556/ https://github.com/pitrou/cpython/tree/threaded_gc https://github.com/pitrou/cpython/

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments