[Deferred] PEP 219 - Stackless Python

원문 링크: PEP 219 - Stackless Python

상태: Deferred 유형: Standards Track 작성일: 14-Aug-2000

PEP 219 – Stackless Python 번역 및 해설

작성자: Gordon McMillan 상태: 연기됨 (Deferred) 유형: 표준 트랙 (Standards Track) 생성일: 2000년 8월 14일 Python 버전: 2.1 최종 수정일: 2025년 2월 1일

서론

이 PEP (Python Enhancement Proposal)는 제너레이터(generators), 마이크로스레드(microthreads), 그리고 코루틴(coroutines)을 효율적으로 지원하기 위해 핵심 Python에 필요한 변경 사항들을 논의합니다. 이 PEP는 이러한 기능들을 지원하도록 Python을 확장하는 방법을 설명하는 PEP 220과 관련이 있습니다. PEP 219의 주된 초점은 이러한 확장이 작동하는 데 필요한 변경 사항에 엄격하게 맞춰져 있습니다.

이 PEP는 Christian Tismer의 Stackless 구현을 기반으로 하지만, Stackless를 유일한 참조 구현으로 보지는 않습니다. Stackless는 (확장 모듈과 함께) 컨티뉴에이션(continuations)을 구현하며, 컨티뉴에이션을 통해 코루틴, 마이크로스레드 (Will Ware에 의해 구현된 바 있음), 그리고 제너레이터를 구현할 수 있습니다. 그러나 1년이 넘는 기간 동안 컨티뉴에이션의 다른 생산적인 용도를 찾지 못했기 때문에, 이에 대한 지원 요구는 없는 것으로 보입니다.

그럼에도 불구하고, Stackless의 컨티뉴에이션 지원은 구현의 비교적 작은 부분에 불과하므로, 이를 “하나의” 참조 구현으로 볼 수도 있습니다.

배경

제너레이터와 코루틴은 다양한 언어에서 여러 방식으로 구현되어 왔습니다. 실제로 Tim Peters는 스레드를 사용하여 순수 Python으로 제너레이터와 코루틴을 구현한 바 있습니다 (Java에도 스레드 기반의 코루틴 구현이 존재합니다). 하지만 스레드 기반 구현의 엄청난 오버헤드는 이 접근 방식의 유용성을 심각하게 제한합니다.

마이크로스레드 (일명 “그린” 스레드 또는 “사용자” 스레드)와 코루틴은 단일 스택을 기반으로 하는 언어 구현에서 수용하기 어려운 제어 전송(transfers of control)을 포함합니다. (제너레이터는 단일 스택에서 구현될 수 있지만, 매우 간단한 형태의 코루틴으로 간주될 수도 있습니다).

실제 스레드는 각 제어 스레드에 대해 전체 크기의 스택을 할당하며, 이것이 오버헤드의 주된 원인입니다. 그러나 코루틴과 마이크로스레드는 거의 오버헤드 없이 Python에서 구현될 수 있습니다. 따라서 이 PEP는 Python이 수천 개의 개별 “스레드” 활동을 현실적으로 관리할 수 있는 방법을 제공합니다 (현재는 수십 개의 개별 스레드 활동으로 제한됨).

이 PEP의 또 다른 정당성 (PEP 220에서 탐구됨)은 코루틴과 제너레이터가 현재 Python에서는 불가능한 방식으로 알고리즘을 보다 직접적으로 표현할 수 있게 해준다는 점입니다.

논의

가장 먼저 주목할 점은 Python이 스택에서 인터프리터 데이터 (일반적인 C 스택 사용)와 Python 데이터 (해석된 프로그램의 상태)를 혼합하여 사용하지만, 이 두 가지는 논리적으로 분리되어 있다는 것입니다. 단지 같은 스택을 사용할 뿐입니다.

실제 스레드는 구현체가 스레드에 얼마나 많은 스택 공간이 필요할지 알 수 없기 때문에 프로세스 크기에 가까운 스택을 얻게 됩니다. 개별 프레임에 필요한 스택 공간은 합리적일 수 있지만, 스택 전환(stack switching)은 C에서 지원되지 않는 난해하고 이식성 없는 과정입니다.

그러나 Python이 C 스택에 Python 데이터를 넣는 것을 멈추면 스택 전환이 쉬워집니다. 이 PEP의 근본적인 접근 방식은 이 두 가지 아이디어를 기반으로 합니다. 첫째, C의 스택 사용과 Python의 스택 사용을 분리합니다. 둘째, 각 프레임에 해당 프레임의 실행을 처리할 수 있는 충분한 스택 공간을 할당합니다.

일반적인 Stackless Python 사용 시에는, 스택 구조가 조각(chunks)으로 나뉘어져 있다는 점을 제외하고는 일반적인 스택 구조를 가집니다. 하지만 코루틴/마이크로스레드 확장이 있는 경우, 동일한 메커니즘이 트리 구조의 스택을 지원합니다. 즉, 확장은 일반적인 “호출/반환” 경로 외부에서 프레임 간의 제어 전송을 지원할 수 있습니다.

문제점

이 접근 방식의 주요 어려움은 C가 Python을 호출하는 경우입니다. 문제는 C 스택이 이제 바이트코드 인터프리터의 중첩된 실행을 포함하게 된다는 것입니다. 이러한 상황에서는 코루틴/마이크로스레드 확장이 다른 인터프리터 호출에 있는 프레임으로 제어를 전송하는 것이 허용될 수 없습니다. 만약 프레임이 잘못된 인터프리터에서 완료되어 C로 복귀하면 C 스택이 손상될 수 있습니다.

이상적인 해결책은 바이트코드 인터프리터의 중첩된 실행이 전혀 필요 없는 메커니즘을 만드는 것입니다. 쉬운 해결책은 코루틴/마이크로스레드 확장들이 이러한 상황을 인지하고 현재 호출 외부로의 전송을 허용하지 않도록 하는 것입니다.

C가 Python을 호출하는 코드를 Python의 구현과 C 확장이라는 두 가지 범주로 나눌 수 있습니다. 그리고 타협책을 제시할 수 있습니다: Python의 내부 사용 (및 노력을 기울이고자 하는 C 확장 작성자)은 더 이상 인터프리터의 중첩된 호출을 사용하지 않을 것입니다. 이러한 노력을 기울이지 않는 확장들은 여전히 안전하지만, 코루틴/마이크로스레드와 잘 호환되지 않을 것입니다.

일반적으로 재귀 호출이 루프로 변환될 때, 약간의 추가적인 장부 정리(bookkeeping)가 필요합니다. 루프는 자체적인 인수 및 결과 “스택”을 유지해야 하는데, 실제 스택은 이제 가장 최근의 것만 담을 수 있기 때문입니다. 코드는 더 장황해질 것인데, 완료 시점이 그리 명확하지 않기 때문입니다. Stackless는 이러한 방식으로 구현되지는 않지만, 동일한 문제들을 다루어야 합니다.

일반 Python에서는 PyEval_EvalCode가 프레임을 빌드하고 실행하는 데 사용됩니다. Stackless Python은 FrameDispatcher라는 개념을 도입합니다. PyEval_EvalCode와 마찬가지로 하나의 프레임을 실행합니다. 그러나 인터프리터는 FrameDispatcher에게 새 프레임이 스왑인되었고 새 프레임이 실행되어야 한다고 신호를 보낼 수 있습니다. 프레임이 완료되면 FrameDispatcher는 백 포인터를 따라 “호출” 프레임으로 돌아갑니다.

따라서 Stackless는 재귀를 루프로 변환하지만, 프레임을 관리하는 것은 FrameDispatcher가 아닙니다. 이는 인터프리터 (또는 자신이 무엇을 하는지 아는 확장)에 의해 수행됩니다.

일반적인 아이디어는 C 코드가 Python 코드를 실행해야 할 때, Python 코드용 프레임을 생성하고, 해당 백 포인터를 현재 프레임으로 설정한다는 것입니다. 그런 다음 프레임을 스왑인하고, FrameDispatcher에 신호를 보낸 다음 자리를 비웁니다. 이제 C 스택은 깨끗해집니다. Python 코드는 다른 프레임으로 제어를 전송할 수 있습니다 (확장이 그 수단을 제공하는 경우).

기본적인 경우에는 이 “마법”이 프로그래머에게 (대부분의 경우 Python 내부 프로그래머에게도) 숨겨질 수 있습니다. 그러나 많은 상황에서 또 다른 수준의 어려움이 발생합니다.

내장 함수 map은 이 접근 방식에 두 가지 장애물을 제시합니다. 단순히 프레임을 구성하고 자리를 비울 수 없는데, 루프가 관련되어 있을 뿐만 아니라 루프의 각 패스마다 “후처리”가 필요하기 때문입니다. 다른 것들과 잘 작동하기 위해 Stackless는 map 자체를 위한 프레임 객체를 구성합니다.

인터프리터의 대부분의 재귀는 이만큼 복잡하지는 않지만, 상당히 자주 일부 “후처리” 작업이 필요합니다. Stackless는 필요한 코드 변경량이 많기 때문에 이러한 상황을 수정하지 않습니다. 대신, Stackless는 중첩된 인터프리터에서 벗어나는 전송을 금지합니다. 이는 이상적이지는 않지만 (때로는 혼란스럽지만), 이 제한 사항은 거의 치명적이지 않습니다.

장점

일반 Python의 경우, 이 접근 방식의 장점은 C 스택 사용량이 훨씬 작고 예측 가능해진다는 것입니다. Python 코드의 무한 재귀는 스택 오류 대신 메모리 오류가 됩니다 (따라서 비-Cupertino 운영 체제에서는 복구 가능한 상황이 됩니다). 물론 대가는 바이트코드 인터프리터 루프의 재귀를 고차 루프로 변환하는 데서 오는 복잡성 (및 수반되는 장부 정리)입니다.

가장 큰 장점은 Python 스택이 실제로는 트리 구조이며, 프레임 디스패처가 트리의 리프 노드(leaf nodes) 간에 자유롭게 제어를 전송할 수 있어 마이크로스레드 및 코루틴과 같은 것을 가능하게 한다는 점입니다.

참고 자료

  • Stackless 웹사이트
  • Will Ware의 uthread 구현 (아카이브 링크)
  • 소스 배포판의 Demo/threads/Generator.py
  • Tim Peters의 코루틴 구현

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

Comments