PEP 734 – 표준 라이브러리의 여러 인터프리터
- Author:
- Eric Snow <ericsnowcurrently at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Created:
- 06-Nov-2023
- Python-Version:
- 3.14
- Post-History:
- 14-Dec-2023
- Replaces:
- 554
- Resolution:
- 05-Jun-2025
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
Note
이 PEP는 본질적으로 PEP 554의 연장선에 있습니다. 그 문서에는 7년에 걸친 논의 동안 부수적인 정보가 많이 쌓였습니다. 이 PEP는 그 내용을 핵심 정보만 남도록 다시 줄인 것입니다. 그 추가 정보의 상당 부분은 여전히 유효하고 유용하지만, 여기서 다루는 구체적인 제안과 직접적인 관련이 없을 뿐입니다.
Note
이 PEP는 이름을 concurrent.interpreters로 변경한다는 조건으로 승인되었습니다.
초록
이 PEP는 현재 프로세스에서 여러 인터프리터를 검사하고, 생성하고, 그 안에서 코드를 실행하는 기능을 지원하기 위해 새로운 모듈 interpreters를 추가할 것을 제안합니다. 여기에는 기반 인터프리터를 나타내는 Interpreter 객체가 포함됩니다. 이 모듈은 인터프리터 간 통신을 위한 기본 Queue 클래스도 제공합니다. 마지막으로, interpreters 모듈을 기반으로 하는 새로운 concurrent.futures.InterpreterPoolExecutor를 추가합니다.
소개
근본적으로 “인터프리터”란 Python 스레드들이 공유해야 하는 (사실상) 모든 런타임 상태의 모음입니다. 그러므로 먼저 스레드를 살펴봅시다. 그런 다음 인터프리터로 다시 돌아오겠습니다.
스레드와 스레드 상태
Python 프로세스에는 Python 코드를 실행하는(또는 다른 방식으로 C API와 상호 작용하는) OS 스레드가 하나 이상 있습니다. 이 스레드들은 각각 자신만의 스레드 상태(PyThreadState)를 사용하여 CPython 런타임과 상호 작용하며, 이 스레드 상태는 해당 스레드에 고유한 모든 런타임 상태를 담고 있습니다. 여러 OS 스레드 사이에서 공유되는 런타임 상태도 일부 있습니다.
모든 OS 스레드는 현재 사용 중인 스레드 상태를 전환할 수 있습니다. 단, 다른 OS 스레드가 이미 사용하고 있는(또는 사용해 온) 스레드 상태로는 전환할 수 없습니다. 이 “현재” 스레드 상태는 런타임이 스레드 지역 변수에 저장하며, PyThreadState_Get()으로 명시적으로 조회할 수 있습니다. 현재 스레드 상태는 최초의(“메인”) OS 스레드와 threading.Thread 객체에 대해 자동으로 설정됩니다. C API에서는 PyThreadState_Swap()으로 설정(및 해제)되며, PyGILState_Ensure()로 설정될 수도 있습니다. C API의 대부분은 현재 스레드 상태가 있어야 동작하며, 이 스레드 상태는 암묵적으로 조회되거나 인자로 전달됩니다.
OS 스레드와 스레드 상태의 관계는 일대다입니다. 각 스레드 상태는 최대 하나의 OS 스레드와 연결되며 해당 스레드의 ID를 기록합니다. 하나의 스레드 상태가 둘 이상의 OS 스레드에 사용되는 일은 결코 없습니다. 그러나 반대 방향으로는 하나의 OS 스레드에 둘 이상의 스레드 상태가 연결될 수 있습니다. 다만 앞서 말했듯이 현재 스레드 상태가 될 수 있는 것은 하나뿐입니다.
하나의 OS 스레드에 스레드 상태가 둘 이상 있으면, 해당 OS 스레드에서 PyThreadState_Swap()을 사용하여 스레드 상태를 전환하며, 요청된 스레드 상태가 현재 스레드 상태가 됩니다. 이전 스레드 상태를 사용하여 그 스레드에서 실행 중이던 작업은 그 스레드 상태로 다시 전환될 때까지 사실상 일시 중지됩니다.
인터프리터 상태
앞서 언급했듯이 여러 OS 스레드가 공유하는 런타임 상태가 일부 있습니다. 그중 일부는 sys 모듈을 통해 노출되지만, 상당 부분은 내부적으로만 사용되어 명시적으로 노출되지 않거나 C API를 통해서만 노출됩니다.
이 공유 상태를 인터프리터 상태(PyInterpreterState)라고 부릅니다. 여기서는 이를 때때로 그냥 “인터프리터”라고 부르겠지만, 이 용어는 python 실행 파일, Python 구현, 바이트코드 인터프리터(즉 exec()/eval())를 가리키는 데에도 쓰입니다.
CPython은 버전 1.5(1997년)부터 같은 프로세스 안에서 여러 인터프리터(이른바 “서브인터프리터”)를 지원해 왔습니다. 이 기능은 C API를 통해 제공되어 왔습니다.
인터프리터와 스레드
스레드 상태와 인터프리터 상태의 관계는 (큰 틀에서 보면) OS 스레드와 프로세스의 관계와 거의 같습니다. 우선 이 관계는 일대다 관계입니다. 스레드 상태는 하나의 인터프리터에 속합니다(그리고 그 인터프리터를 가리키는 포인터를 저장합니다). 그 스레드 상태가 다른 인터프리터에 사용되는 일은 결코 없습니다. 하지만 반대 방향으로 보면, 인터프리터에는 연결된 스레드 상태가 0개 이상 있을 수 있습니다. 인터프리터는 자신의 스레드 상태 중 하나가 현재 스레드 상태인 OS 스레드에서만 활성 상태로 간주됩니다.
인터프리터는 C API의 Py_NewInterpreterFromConfig() (또는 Py_NewInterpreterFromConfig()를 가볍게 감싼 래퍼인 Py_NewInterpreter())를 사용하여 생성됩니다. 이 함수는 다음 작업을 수행합니다.
- 새 인터프리터 상태를 생성합니다.
- 새 스레드 상태를 생성합니다.
- 스레드 상태를 현재 스레드 상태로 설정합니다(인터프리터 초기화에는 현재 tstate가 필요합니다).
- 그 스레드 상태를 사용하여 인터프리터 상태를 초기화합니다.
- 스레드 상태를 반환합니다(여전히 현재 스레드 상태입니다).
반환된 스레드 상태는 즉시 폐기해도 된다는 점에 유의하십시오. 인터프리터를 실제로 사용하려는 시점이 되기 전까지는 인터프리터에 스레드 상태가 있어야 한다는 요구 사항이 없습니다. 그 시점에는 인터프리터를 현재 OS 스레드에서 활성 상태로 만들어야 합니다.
기존 인터프리터를 현재 OS 스레드에서 활성 상태로 만들려면, C API 사용자는 먼저 그 인터프리터에 대응하는 스레드 상태가 있는지 확인합니다. 그런 다음 그 스레드 상태를 사용하여 평소처럼 PyThreadState_Swap()을 호출합니다. 다른 인터프리터의 스레드 상태가 이미 현재 스레드 상태였다면 그 스레드 상태는 평소처럼 교체되어 빠지며, 따라서 해당 OS 스레드에서 그 인터프리터의 실행은 다시 교체되어 들어올 때까지 사실상 일시 중지됩니다.
이렇게 인터프리터가 현재 OS 스레드에서 활성 상태가 되면, 그 스레드는 PyEval_EvalCode()(즉, exec())와 같은 어떤 C API든 호출할 수 있습니다. 이는 현재 스레드 상태를 런타임 컨텍스트로 사용하는 방식으로 동작합니다.
“메인” 인터프리터
Python 프로세스가 시작되면 현재 OS 스레드를 위한 단일 스레드 상태와 함께 단일 인터프리터 상태(“메인” 인터프리터)를 생성합니다. 그런 다음 이들을 사용하여 Python 런타임을 초기화합니다.
초기화가 끝나면 이를 사용하여 스크립트, 모듈 또는 REPL이 실행됩니다. 이 실행은 인터프리터의 __main__ 모듈에서 이루어집니다.
프로세스가 메인 OS 스레드에서 요청된 Python 코드나 REPL의 실행을 마치면, 해당 스레드에서 메인 인터프리터를 사용하여 Python 런타임이 종료 처리됩니다.
런타임 종료 처리는 아직 실행 중인 Python 스레드에 약간의 간접적인 영향만 미치며, 이는 스레드가 메인 인터프리터에 있든 서브인터프리터에 있든 마찬가지입니다. 종료 처리가 시작되자마자 데몬이 아닌 모든 Python 스레드가 끝날 때까지 무기한 기다리기 때문입니다.
C API로 조회할 수는 있지만, threading._register_atexit()를 사용하여 등록되었을 수 있는 “atexit” 함수를 제외하면 종료 처리가 시작되었음을 Python 스레드에 직접 알리는 메커니즘은 없습니다.
남아 있는 서브인터프리터들도 나중에 종료 처리되지만, 그 시점에는 어떤 OS 스레드에서도 현재 인터프리터가 아닙니다.
인터프리터 격리
CPython의 인터프리터들은 서로 엄격하게 격리되도록 설계되었습니다. 즉, 인터프리터들은 결코 객체를 공유하지 않습니다(불멸이면서 불변인 내장 객체와 관련된 아주 특수한 경우는 예외입니다). 각 인터프리터는 자체 모듈(sys.modules), 클래스, 함수, 변수를 가집니다. 두 인터프리터가 같은 클래스를 정의하더라도 각자 자신만의 사본을 가집니다. 확장 모듈을 포함하여 C 수준의 상태에도 똑같이 적용됩니다. CPython C API 문서에서 더 자세히 설명합니다(explain more).
특히, 인터프리터들이 항상 공유하게 되는 프로세스 전역 상태가 일부 있으며, 그중 일부는 가변이고 일부는 불변입니다. 불변 상태를 공유하는 것은 문제가 거의 없으면서도 몇 가지 이점(주로 성능)을 제공합니다. 그러나 공유되는 가변 상태는 모두 특히 스레드 안전성 측면에서 특별한 관리가 필요하며, 그중 일부는 OS가 대신 처리해 줍니다.
가변:
- 파일 디스크립터
- 저수준 환경 변수
- 프로세스 메모리(단, 할당자는 격리되어 있습니다)
- 인터프리터 목록
불변:
- 내장 타입(예:
dict,bytes) - 싱글턴(예:
None) - 내장/확장/프로즌 모듈의 기반이 되는 정적 모듈 데이터(예: 함수)
기존 실행 구성 요소
Python에는 서브인터프리터에서 코드가 어떻게 실행될 수 있는지 이해하는 데 도움이 될 만한 기존 구성 요소가 여럿 있습니다.
CPython에서 각 구성 요소는 다음 C API 함수(또는 그 변형) 중 하나를 중심으로 만들어져 있습니다.
PyEval_EvalCode(): 주어진 코드 객체로 바이트코드 인터프리터를 실행PyRun_String(): 컴파일 +PyEval_EvalCode()PyRun_File(): 읽기 + 컴파일 +PyEval_EvalCode()PyRun_InteractiveOneObject(): 컴파일 +PyEval_EvalCode()PyObject_Call():PyEval_EvalCode()를 호출합니다
builtins.exec()
내장 exec()를 사용하여 Python 코드를 실행할 수 있습니다. 이 함수는 본질적으로 C API 함수 PyRun_String()과 PyEval_EvalCode()를 감싼 래퍼입니다.
내장 함수 exec()의 관련 특성은 다음과 같습니다:
- 현재 OS 스레드에서 실행되며, 그 스레드에서 실행 중이던 작업을 일시 중지시킵니다. 중지된 작업은
exec()가 끝나면 재개됩니다. 다른 OS 스레드는 영향을 받지 않습니다. (현재 Python 스레드가 일시 정지되지 않게 하려면exec()를threading.Thread에서 실행하십시오.) - 추가 스레드를 시작할 수 있으며, 이 스레드들은 실행을 중단시키지 않습니다.
- “globals” 네임스페이스(및 “locals” 네임스페이스)를 대상으로 실행됩니다. 모듈 수준에서
exec()는 기본적으로 현재 모듈의__dict__(즉,globals())를 사용합니다.exec()는 해당 네임스페이스를 그대로 사용하며, 실행 전후에 이를 비우지 않습니다. - 실행한 코드에서 잡히지 않은 예외를 모두 전파합니다. 예외는 원래
exec()를 호출한 Python 스레드에서exec()호출로부터 발생합니다.
명령줄
python CLI는 파이썬 코드를 실행하는 여러 방법을 제공합니다. 각 방법은 대응하는 C API 호출에 매핑됩니다:
<no args>,-i- REPL을 실행합니다(PyRun_InteractiveOneObject()).<filename>- 스크립트를 실행합니다(PyRun_File()).-c <code>- 주어진 Python 코드 실행 (PyRun_String())-m module- 모듈을 스크립트로 실행합니다(runpy._run_module_as_main()을 통한PyEval_EvalCode()).
각 경우 모두 본질적으로 메인 인터프리터의 __main__ 모듈 최상위 수준에서 exec()를 실행하는 것의 변형입니다.
threading.Thread
Python 스레드가 시작되면 새 스레드 상태를 사용하여 PyObject_Call()로 “대상” 함수를 실행합니다. globals 네임스페이스는 func.__globals__에서 가져오며, 잡히지 않은 예외는 모두 버려집니다.
동기
interpreters 모듈은 여러 인터프리터 기능에 대한 고수준 인터페이스를 제공할 것입니다. 목표는 CPython의 기존 다중 인터프리터 기능을 파이썬 코드에서 더 쉽게 사용할 수 있도록 하는 것입니다. 이는 CPython에 인터프리터별 GIL(PEP 684)이 생기고 사람들이 여러 인터프리터를 사용하는 데 더 많은 관심을 보이는 지금 특히 중요합니다.
표준 라이브러리 모듈이 없으면 사용자는 C API만 사용할 수 있으며, 이로 인해 여러 인터프리터를 시험해 보고 활용할 수 있는 범위가 제한됩니다.
이 모듈에는 인터프리터 간 통신을 위한 기본 메커니즘이 포함될 것입니다. 그러한 메커니즘이 없다면 여러 인터프리터는 훨씬 덜 유용한 기능이 됩니다.
명세
이 모듈은 다음을 수행합니다:
- 기존의 여러 인터프리터 지원을 노출
- 인터프리터 간 통신을 위한 기본 메커니즘을 도입합니다
이 모듈은 새로운 저수준 _interpreters 모듈을 래핑합니다(threading 모듈과 같은 방식입니다). 그러나 그 저수준 API는 공개적으로 사용하도록 의도된 것이 아니므로 이 제안에 포함되지 않습니다.
인터프리터 사용하기
이 모듈은 다음 함수들을 정의합니다:
get_current() -> Interpreter- 현재 실행 중인 인터프리터에 대한
Interpreter객체를 반환합니다.
list_all() -> list[Interpreter]- 현재 어떤 OS 스레드에서 실행 중인지 여부와 관계없이, 존재하는 각 인터프리터에 대한
Interpreter객체를 반환합니다.
create() -> Interpreter- 새 인터프리터를 생성하고 그에 대한
Interpreter객체를 반환합니다. 인터프리터는 스스로 아무것도 하지 않으며 본질적으로 어떤 OS 스레드에도 묶여 있지 않습니다. 그러한 일은 인터프리터에서 실제로 무언가가 실행될 때(예:Interpreter.exec())에만, 그리고 실행되는 동안에만 일어납니다. 인터프리터에는 사용할 준비가 된 스레드 상태가 있을 수도 있고 없을 수도 있지만, 이는 엄밀히 내부 구현 세부 사항입니다.
인터프리터 객체
interpreters.Interpreter 객체는 해당 고유 ID를 가진 인터프리터(PyInterpreterState)를 나타냅니다. 주어진 인터프리터마다 객체는 하나만 존재합니다.
인터프리터가 interpreters.create()로 생성되었다면, 해당 ID를 가진 모든 Interpreter 객체가 (모든 인터프리터에 걸쳐) 삭제되는 즉시 그 인터프리터는 파괴됩니다.
Interpreter 객체는 interpreters.create()로 생성된 인터프리터 이외의 다른 인터프리터를 나타낼 수도 있습니다. 예로는 (Python 런타임 초기화로 생성되는) 메인 인터프리터와 C-API에서 Py_NewInterpreter()를 사용하여 생성한 인터프리터가 있습니다. 그러한 Interpreter 객체는 해당 인터프리터와 상호 작용할 수 없습니다. 예를 들어 Interpreter.exec()를 통한 상호 작용이 불가능합니다(향후 이 제한을 완화할 수도 있습니다).
속성과 메서드:
id- (읽기 전용) 이
Interpreter인스턴스가 나타내는 인터프리터를 식별하는 음이 아닌int입니다. 개념적으로 이는 프로세스 ID와 비슷합니다.
__hash__()- 인터프리터의
id의 해시를 반환합니다. 이는 ID의 정수 값의 해시와 같습니다.
is_running() -> bool- 인터프리터가 현재 자신의
__main__모듈에서 코드를 실행하고 있으면True를 반환합니다. 하위 스레드는 여기에 포함되지 않습니다.이는 인터프리터의
__main__모듈에서 스크립트(코드)를 실행하고 있는 OS 스레드가 있는지 여부만을 가리킵니다. 기본적으로 어떤 OS 스레드에서든Interpreter.exec()가 실행 중인지 여부를 의미합니다. 하위 스레드에서 실행 중인 코드는 무시됩니다.
prepare_main(**kwargs)- 인터프리터의
__main__모듈에 하나 이상의 객체를 바인딩합니다.키워드 인자 이름이 속성 이름으로 사용됩니다. 대부분의 객체는 중간에 pickle을 거쳐 복사본이 인터프리터에 바인딩됩니다.
memoryview같은 일부 객체는 하부 데이터가 인터프리터 간에 공유됩니다. Shareable Objects를 참조하십시오.prepare_main()은 인터프리터에서 코드를 실행하기 전에 해당 인터프리터의 전역 변수를 초기화하는 데 유용합니다.
exec(code, /)- 주어진 소스 코드를 인터프리터의
__main__모듈을 사용하여 해당 인터프리터에서 (현재 OS 스레드에서) 실행합니다. 아무것도 반환하지 않습니다.이는 본질적으로 현재 OS 스레드에서 이 인터프리터로 전환한 다음, 이 인터프리터의
__main__모듈의__dict__를 전역 및 지역 변수로 사용하여 내장exec()를 호출하는 것과 같습니다.현재 OS 스레드에서 실행 중인 코드(다른 인터프리터의 코드)는
Interpreter.exec()가 끝날 때까지 사실상 일시 중지됩니다. 일시 중지를 피하려면 새threading.Thread를 만들고 그 안에서Interpreter.exec()를 호출하십시오(Interpreter.call_in_thread()가 하는 것처럼).Interpreter.exec()는 실행 전에도 후에도 인터프리터의 상태나__main__모듈을 재설정하지 않으므로, 이어지는 각 호출은 직전 호출이 끝난 지점에서 계속됩니다. 이는 나중에 어떤 반복 작업을 수행하기 전에 인터프리터를 초기화하는 코드(예: 임포트)를 실행할 때 유용할 수 있습니다.처리되지 않은 예외가 있으면 그 예외는
ExecutionFailed로서 호출한 인터프리터로 전파됩니다. 호출된 인터프리터를 기준으로 생성된 원래 예외의 전체 오류 표시는 전파된ExecutionFailed에 보존됩니다. 여기에는 문법 오류 세부 정보나 연쇄된 예외와 같은 모든 추가 정보를 포함한 전체 트레이스백이 포함됩니다.ExecutionFailed가 잡히지 않으면, 전파된 예외가 메인 인터프리터에서 발생하고 잡히지 않았을 때와 거의 마찬가지로 그 전체 오류 표시가 출력됩니다. 전체 트레이스백이 있으면 디버깅할 때 특히 유용합니다.예외 전파를 원하지 않는다면
Interpreter.exec()에 전달하는 code를 명시적인 try-except로 감싸야 합니다. 마찬가지로 예외의 특정 정보에 의존하는 오류 처리는 모두 주어진 code를 명시적인 try-except로 감싸야 합니다.ExecutionFailed는 그 정보를 보존하지 않기 때문입니다.
call(callable, /)- 인터프리터에서 호출 가능 객체를 호출합니다. 반환값은 버려집니다. 호출 가능 객체가 예외를 발생시키면
Interpreter.exec()와 같은 방식으로ExecutionFailed예외로서 전파됩니다.현재는 일반 함수만 지원하며, 그중에서도 인자를 받지 않고 셀 변수가 없는 함수만 지원합니다. 자유 전역 변수는 대상 인터프리터의
__main__모듈을 기준으로 해석됩니다.앞으로는 적어도 일부는 pickle을 통해 인자, 클로저, 그리고 더 다양한 호출 가능 객체에 대한 지원을 추가할 수 있습니다. 반환값을 버리지 않는 방안도 고려할 수 있습니다. 이러한 초기 제약은 모듈의 기본 기능을 사용자에게 더 빨리 제공하기 위해 마련된 것입니다.
call_in_thread(callable, /) -> threading.Thread- 본질적으로 새 스레드에서
Interpreter.call()을 적용합니다. 반환값은 버려지며 예외는 전파되지 않습니다.call_in_thread()는 대략 다음과 동등합니다.:def task(): interp.call(func) t = threading.Thread(target=task) t.start()
close()- 기반 인터프리터를 파괴합니다.
인터프리터 간 통신
이 모듈은 특수한 큐를 통한 기본적인 통신 메커니즘을 도입합니다.
interpreters.Queue 객체가 있지만, 이 객체는 실제 자료 구조, 즉 어느 한 인터프리터의 바깥에 존재하는 크기 제한 없는 FIFO 큐를 프록시할 뿐입니다. 이 큐에는 인터프리터 격리를 위반하지 않으면서 인터프리터 간에 객체 데이터를 안전하게 전달하기 위한 특별한 장치가 마련되어 있습니다. 여기에는 스레드 안전성도 포함됩니다.
Python의 다른 큐와 마찬가지로, “put”을 할 때마다 객체가 뒤쪽에 추가되고 “get”을 할 때마다 앞쪽에서 다음 객체를 꺼냅니다. 추가된 모든 객체는 넣은 순서대로 꺼내집니다.
pickle할 수 있는 객체라면 무엇이든 interpreters.Queue를 통해 보낼 수 있습니다.
실제 객체가 보내지는 것이 아니라 그 기반 데이터가 보내진다는 점에 유의하십시오. 결과로 만들어지는 객체는 원본과 엄밀하게 동등합니다. 대부분의 객체에서는 기반 데이터가 직렬화(예: pickle)됩니다. memoryview처럼 몇몇 경우에는 기반 데이터가 직렬화 없이 보내집니다(그리고 공유됩니다). Shareable Objects를 참조하십시오.
이 모듈은 다음 함수를 정의합니다.
create_queue(maxsize=0) -> Queue- 새 큐를 만듭니다. maxsize가 0이거나 음수이면 큐의 크기에 제한이 없습니다.
큐 객체
interpreters.Queue 객체는 interpreters 모듈이 노출하는, 인터프리터 간에 안전한 기반 큐의 프록시 역할을 합니다. 각 Queue 객체는 해당하는 고유 ID를 가진 큐를 나타냅니다. 주어진 큐마다 객체는 하나만 존재합니다.
Queue는 task_done()과 join()을 제외한 queue.Queue의 모든 메서드를 구현하므로, asyncio.Queue 및 multiprocessing.Queue와 유사합니다.
속성 및 메서드:
id- (읽기 전용) 해당 인터프리터 간 큐를 식별하는 음이 아닌
int입니다. 개념적으로 이는 파이프에 사용되는 파일 디스크립터와 유사합니다.
maxsize- (읽기 전용) 큐에 허용되는 항목의 수입니다. 0은 “무제한”을 의미합니다.
__hash__()- 큐의
id에 대한 해시를 반환합니다. 이는 ID의 정수 값의 해시와 같습니다.
empty()- 큐가 비어 있으면
True를, 그렇지 않으면False를 반환합니다.이는 호출 시점의 상태를 나타내는 스냅숏일 뿐입니다. 다른 스레드나 인터프리터로 인해 이 상태가 바뀔 수 있습니다.
full()- 큐에
maxsize개의 항목이 있으면True를 반환합니다.큐가
maxsize=0(기본값)으로 초기화되었다면full()은 절대True를 반환하지 않습니다.이는 호출 시점의 상태를 나타내는 스냅숏일 뿐입니다. 다른 스레드나 인터프리터로 인해 이 상태가 바뀔 수 있습니다.
qsize()- 큐에 있는 항목의 수를 반환합니다.
이는 호출 시점의 상태를 나타내는 스냅숏일 뿐입니다. 다른 스레드나 인터프리터로 인해 이 상태가 바뀔 수 있습니다.
put(obj, timeout=None)- 객체를 큐에 추가합니다.
maxsize > 0이고 큐가 가득 차 있으면 빈 슬롯이 생길 때까지 블록합니다. 타임아웃 값 timeout이 양수이면 최소한 그 초만큼만 블록한 다음interpreters.QueueFull을 발생시킵니다. 그렇지 않으면 무기한 블록합니다.거의 모든 객체를 큐를 통해 보낼 수 있습니다.
memoryview의 경우처럼 일부 경우에는 기반 데이터가 단순히 복사되는 것이 아니라 실제로 공유됩니다. Shareable Objects를 참조하십시오.객체가 아직 큐에 있는데 그 객체를 큐에 넣은(즉, 객체가 속한) 인터프리터가 소멸되면, 그 객체는 즉시 큐에서 제거됩니다. (나중에 큐에서 제거된 객체를 센티널로 대체하거나 대응하는
get()호출에서 예외를 발생시키는 옵션을 추가할 수도 있습니다.)
put_nowait(objput()과 같지만 사실상 즉시 타임아웃이 적용됩니다. 따라서 큐가 가득 차 있으면 즉시interpreters.QueueFull을 발생시킵니다.
get(timeout=None) -> object- 큐에서 다음 객체를 꺼내 반환합니다. 큐가 비어 있는 동안 블록합니다. 양수인 타임아웃 값 timeout이 주어지고 그 초 동안 큐에 객체가 추가되지 않으면
interpreters.QueueEmpty를 발생시킵니다.
get_nowait() -> objectget()과 같지만 블록하지 않습니다. 큐가 비어 있지 않으면 다음 항목을 반환합니다. 그렇지 않으면interpreters.QueueEmpty를 발생시킵니다.
공유 가능한 객체
“공유 가능한” 객체는 한 인터프리터에서 다른 인터프리터로 전달될 수 있는 객체입니다. 그 객체가 실제로 인터프리터 간에 직접 공유되지는 않습니다. 하지만 공유된 객체는 가변성에 관한 주의 사항은 있지만 직접 공유되는 것처럼 취급해야 합니다.
피클링할 수 있는 모든 객체는 공유 가능합니다. 따라서 거의 모든 객체가 공유 가능합니다. interpreters.Queue 객체도 공유 가능합니다.
interp.prepare_main()으로든 queue.put()으로든, 객체를 인터프리터로 보내는 거의 모든 경우에 실제 객체는 전송되지 않습니다. 대신 객체의 기반 데이터가 전송됩니다. 대부분의 객체는 피클링되고, 받는 인터프리터가 이를 언피클링합니다.
주목할 만한 예외는 memoryview처럼 “버퍼” 프로토콜을 구현하는 객체입니다. 이러한 객체의 기반 Py_buffer는 실제로 인터프리터 간에 공유됩니다. interp.prepare_main()과 queue.get()은 그 버퍼를 새 memoryview 객체로 감쌉니다.
대부분의 가변 객체는 다른 인터프리터로 보내질 때 복사됩니다. 따라서 원본이나 복사본에 대한 변경 사항은 절대 다른 쪽에 동기화되지 않습니다. 피클링을 통해 공유되는 가변 객체가 이 범주에 속합니다. 하지만 interpreters.Queue와 버퍼 프로토콜을 구현하는 객체는 기반 데이터가 인터프리터 간에 실제로 공유되는 주목할 만한 경우이므로, 객체가 동기화된 상태로 유지됩니다.
인터프리터들이 가변 데이터를 실제로 공유할 때는 항상 데이터 경쟁의 위험이 있습니다. 스레드 안전성을 포함한 인터프리터 간 안전성은 interpreters.Queue의 핵심 기능입니다.
하지만 버퍼 프로토콜(즉, Py_buffer)에는 데이터 경쟁에 대비한 자체적인 장치가 전혀 없습니다. 대신 큐를 통해 토큰을 주고받아 안전함을 표시하든(Synchronization 참고) 개별 인터프리터에 하위 범위의 독점권을 할당하든, 스레드 안전성 관리는 사용자의 책임입니다.
인터프리터들은 서로 정보를 주고받으므로, 대부분의 객체는 큐(interpreters.Queue)를 통해 공유됩니다. 그보다 드물게는, 인터프리터에서 코드를 실행하기 전에 그 인터프리터를 준비하기 위해 prepare_main()을 통해 객체를 공유합니다. 하지만 prepare_main()은 큐를 공유하는 주된 방법이며, 이를 통해 다른 인터프리터에 이후의 통신 수단을 제공합니다.
동기화
두 인터프리터를 동기화해야 하는 상황이 있습니다. 여기에는 자원 공유, 워커 관리, 순차적 일관성 유지 등이 포함될 수 있습니다.
스레드 프로그래밍에서 전형적인 동기화 프리미티브는 뮤텍스 같은 타입입니다. threading 모듈은 이러한 프리미티브를 여러 가지 제공합니다. 하지만 인터프리터는 객체를 공유할 수 없으므로 threading.Lock 객체도 공유할 수 없습니다.
interpreters 모듈은 그러한 전용 동기화 프리미티브를 제공하지 않습니다. 대신 interpreters.Queue 객체가 필요한 모든 것을 제공합니다.
예를 들어 접근을 관리해야 하는 공유 자원이 있다면 큐를 사용해 관리할 수 있으며, 이때 인터프리터들은 누가 그 자원을 사용할 수 있는지 나타내는 객체를 서로 주고받습니다:
import interpreters
from mymodule import load_big_data, check_data
numworkers = 10
control = interpreters.create_queue()
data = memoryview(load_big_data())
def worker():
interp = interpreters.create()
interp.prepare_main(control=control, data=data)
interp.exec("""if True:
from mymodule import edit_data
while True:
token = control.get()
edit_data(data)
control.put(token)
""")
threads = [threading.Thread(target=worker) for _ in range(numworkers)]
for t in threads:
t.start()
token = 'football'
control.put(token)
while True:
control.get()
if not check_data(data):
break
control.put(token)
예외
InterpreterError- 인터프리터와 관련된 실패가 발생했음을 나타냅니다.
이 예외는
Exception의 서브클래스입니다.
InterpreterNotFoundError- 기반 인터프리터가 (예를 들어 C-API를 통해) 파괴된 후에
Interpreter메서드에서 발생합니다.이 예외는
InterpreterError의 서브클래스입니다.
ExecutionFailed- 처리되지 않은 예외가 있을 때
Interpreter.exec()및Interpreter.call()에서 발생합니다. 이 예외의 오류 표시에는 처리되지 않은 예외의 트레이스백이 포함되며, 이 트레이스백은ExceptionGroup에서와 마찬가지로 일반 오류 표시 뒤에 표시됩니다.속성:
type-__name__,__module__,__qualname__어트리뷰트를 가진, 원래 예외 클래스의 표현msg- 원래 예외의str(exc)snapshot- 원래 예외에 대한traceback.TracebackException객체
이 예외는
InterpreterError의 서브클래스입니다.
QueueError- 큐와 관련된 실패가 발생했음을 나타냅니다.
이 예외는
Exception의 서브클래스입니다.
QueueNotFoundError- 기반 큐가 파괴된 후에
interpreters.Queue메서드에서 발생합니다.이 예외는
QueueError의 서브클래스입니다.
QueueEmpty- 큐가 비어 있을 때
Queue.get()(또는 기본값 없이 호출한get_nowait())에서 발생합니다.이 예외는
QueueError와 표준 라이브러리queue.Empty모두의 서브클래스입니다.
QueueFull- 큐가 이미 최대 크기에 도달했을 때 (타임아웃을 지정한)
Queue.put()또는put_nowait()에서 발생합니다.이 예외는
QueueError와 표준 라이브러리queue.Empty모두의 서브클래스입니다.
InterpreterPoolExecutor
새로운 interpreters 모듈과 함께 새로운 concurrent.futures.InterpreterPoolExecutor도 추가됩니다. 이는 ThreadPoolExecutor에서 파생된 것으로, 각 워커는 자신의 스레드에서 실행되지만 워커마다 고유한 서브인터프리터를 가집니다.
다른 실행기와 마찬가지로 InterpreterPoolExecutor는 작업과 초기화 함수로 호출 가능 객체를 지원합니다. 또한 다른 실행기와 마찬가지로 두 경우 모두 인자에 대한 제한이 거의 없습니다. 호출 가능 객체와 인자는 워커의 인터프리터로 전송될 때 ProcessPoolExecutor가 동작하는 방식처럼 일반적으로 pickle 등으로 직렬화됩니다. 이는 (적어도 초기에는) 훨씬 더 제한적인 Interpreter.call()과 대조됩니다.
워커 간의 통신이나 실행기(또는 일반적으로 그 인터프리터)와 워커 간의 통신은 여전히 초기화 함수로 설정한 interpreters.Queue 객체를 통해 이루어질 수 있습니다.
sys.implementation.supports_isolated_interpreters
Python 구현이 서브인터프리터를 반드시 지원해야 하는 것은 아니지만, 대부분의 주요 구현은 지원합니다. 구현이 서브인터프리터를 지원하면 sys.implementation.supports_isolated_interpreters가 True로 설정됩니다. 그렇지 않으면 False가 됩니다. 이 기능이 지원되지 않으면 interpreters 모듈을 임포트할 때 ImportError가 발생합니다.
예시
다음 예제들은 여러 인터프리터가 유용하게 쓰일 수 있는 실제 사례를 보여 줍니다.
예제 1:
하위 스레드의 워커를 통해 처리될 요청 스트림이 들어오고 있습니다.
- 각 워커 스레드는 고유한 인터프리터를 가집니다
- 워커에게 작업을 보내는 큐 하나와 결과를 반환하는 큐 하나가 따로 있습니다
- 결과는 전용 스레드에서 처리됩니다
- 각 워커는 “중지” 센티널(
None)을 받을 때까지 계속 동작합니다 - 결과 핸들러는 모든 워커가 멈출 때까지 계속 동작합니다
import interpreters
from mymodule import iter_requests, handle_result
tasks = interpreters.create_queue()
results = interpreters.create_queue()
numworkers = 20
threads = []
def results_handler():
running = numworkers
while running:
try:
res = results.get(timeout=0.1)
except interpreters.QueueEmpty:
# No workers have finished a request since last time.
pass
else:
if res is None:
# A worker has stopped.
running -= 1
else:
handle_result(res)
empty = object()
assert results.get_nowait(empty) is empty
threads.append(threading.Thread(target=results_handler))
def worker():
interp = interpreters.create()
interp.prepare_main(tasks=tasks, results=results)
interp.exec("""if True:
from mymodule import handle_request, capture_exception
while True:
req = tasks.get()
if req is None:
# Stop!
break
try:
res = handle_request(req)
except Exception as exc:
res = capture_exception(exc)
results.put(res)
# Notify the results handler.
results.put(None)
""")
threads.extend(threading.Thread(target=worker) for _ in range(numworkers))
for t in threads:
t.start()
for req in iter_requests():
tasks.put(req)
# Send the "stop" signal.
for _ in range(numworkers):
tasks.put(None)
for t in threads:
t.join()
예제 2:
이 사례는 여러 워커가 하위 스레드에서 실행된다는 점에서 앞의 사례와 비슷합니다. 하지만 이번에는 코드가 큰 데이터 배열을 여러 청크로 나누고, 각 워커가 한 번에 한 청크씩 처리합니다. 그 데이터를 각 인터프리터에 복사하는 것은 매우 비효율적이므로, 코드는 memoryview 버퍼를 직접 공유하는 방식을 활용합니다.
- 모든 인터프리터가 원본 배열의 버퍼를 공유합니다
- 각 인터프리터는 두 번째 공유 버퍼에 결과를 기록합니다
- 워커에게 작업을 보내는 데 큐를 사용합니다
- 원본 배열의 특정 인덱스는 오직 하나의 워커만 읽습니다
- 결과의 특정 인덱스에는 오직 하나의 워커만 기록합니다 (이 방식으로 스레드 안전성을 보장합니다)
import interpreters
import queue
from mymodule import read_large_data_set, use_results
numworkers = 3
data, chunksize = read_large_data_set()
buf = memoryview(data)
numchunks = (len(buf) + 1) / chunksize
results = memoryview(b'\0' * numchunks)
tasks = interpreters.create_queue()
def worker(id):
interp = interpreters.create()
interp.prepare_main(data=buf, results=results, tasks=tasks)
interp.exec("""if True:
from mymodule import reduce_chunk
while True:
req = tasks.get()
if res is None:
# Stop!
break
resindex, start, end = req
chunk = data[start: end]
res = reduce_chunk(chunk)
results[resindex] = res
""")
threads = [threading.Thread(target=worker) for _ in range(numworkers)]
for t in threads:
t.start()
for i in range(numchunks):
# Assume there's at least one worker running still.
start = i * chunksize
end = start + chunksize
if end > len(buf):
end = len(buf)
tasks.put((start, end, i))
# Send the "stop" signal.
for _ in range(numworkers):
tasks.put(None)
for t in threads:
t.join()
use_results(results)
근거
최소한의 API
코어 개발 팀은 사용자가 Python 코드에서 여러 인터프리터를 어떻게 활용할지에 대한 실제 경험이 없으므로, 이 제안은 의도적으로 초기 API를 가능한 한 간결하고 최소한으로 유지합니다. 목표는 이후 필요에 따라 추가적인 (더 고급) 기능을 더할 수 있는, 충분히 숙고된 기반을 제공하는 것입니다.
그렇기는 하지만, 제안된 설계에는 커뮤니티의 기존 서브인터프리터 사용 사례, 기존 표준 라이브러리 모듈, 그리고 다른 프로그래밍 언어에서 얻은 교훈이 반영되어 있습니다. 또한 CPython 테스트 스위트에서 서브인터프리터를 사용한 경험과 concurrency benchmarks에서 서브인터프리터를 사용한 경험도 고려합니다.
create(), create_queue()
일반적으로 사용자는 타입을 호출하여 그 타입의 인스턴스를 생성하며, 이 시점에 객체의 자원이 할당됩니다. interpreters 모듈은 다른 접근 방식을 취하여, 사용자는 새 인터프리터를 얻으려면 create()를, 새 큐를 얻으려면 create_queue()를 호출해야 합니다. interpreters.Interpreter()를 직접 호출하면 기존 인터프리터를 감싸는 래퍼만 반환됩니다 (interpreters.Queue()도 마찬가지입니다).
이는 인터프리터(와 큐)가 특별한 자원이기 때문입니다. 이들은 프로세스 전역에 존재하며 현재 인터프리터가 관리하거나 소유하지 않습니다. 따라서 interpreters 모듈은 인터프리터(또는 큐)를 생성하는 것을 interpreters.Interpreter (또는 interpreters.Queue)의 인스턴스를 생성하는 것과 눈에 띄게 구별되는 연산으로 만듭니다.
Interpreter.prepare_main()은 여러 변수를 설정합니다
prepare_main()은 일종의 설정자(setter) 함수로 볼 수 있습니다. 대부분의 설정자는 한 번에 하나의 항목만 설정하지만, 이 함수는 interp.prepare_main(spam=1, eggs=2)처럼 여러 이름을 한 번에 설정할 수 있습니다. 주된 이유는 효율성입니다.
인터프리터의 __main__.__dict__에 값을 설정하려면, 구현은 먼저 OS 스레드를 해당 인터프리터로 전환해야 하며, 여기에는 무시할 수 없는 오버헤드가 따릅니다. 값을 설정한 후에는 다시 원래대로 전환해야 합니다. 게다가 인터프리터 간에 객체를 전달하는 메커니즘에도 추가적인 오버헤드가 있는데, 여러 값을 한 번에 설정하면 이를 전체적으로 줄일 수 있습니다.
따라서 prepare_main()은 여러 값을 한 번에 설정하는 것을 지원합니다.
예외 전파
Interpreter.exec()를 통해 서브인터프리터에서 발생한 처리되지 않은 예외는 threading.Thread()처럼 (사실상) 무시될 수도 있고, 내장 exec()처럼 전파될 수도 있습니다. Interpreter.exec()는 내장 exec()와 마찬가지로 동기 연산이므로, 처리되지 않은 예외는 전파됩니다.
그러나 이러한 예외는 직접 발생하지 않습니다. 인터프리터는 서로 격리되어 있으며 예외를 포함한 어떤 객체도 공유해서는 안 되기 때문입니다. 이 문제는 예외의 대리 객체를 발생시켜 해결할 수 있으며, 그 대리 객체는 요약본, 복사본, 예외를 감싸는 프록시 중 어느 것이든 될 수 있습니다. 이 중 어느 것이든 디버깅에 유용한 트레이스백을 보존할 수 있습니다. 발생하는 ExecutionFailed가 바로 그러한 대리 객체입니다.
고려해야 할 또 다른 문제가 있습니다. 전파된 예외가 즉시 잡히지 않으면, 잡힐 때까지(또는 끝내 잡히지 않은 채로) 호출 스택을 따라 위로 전파됩니다. 다른 곳의 코드가 그 예외를 잡을 수도 있는 경우에는, 해당 예외가 현재 인터프리터가 아니라 서브인터프리터(즉, “원격” 출처)에서 왔음을 식별할 수 있으면 도움이 됩니다. 그래서 Interpreter.exec()는 ExecutionFailed를 발생시키며, 이 예외는 원래 예외와 일치하는 클래스를 가진 복사본이나 프록시가 아니라 평범한 Exception입니다. 예를 들어, 서브인터프리터에서 발생해 잡히지 않은 ValueError는 이후의 try: ... except ValueError: ...에서 결코 잡히지 않습니다. 그 대신 ExecutionFailed를 직접 처리해야 합니다.
반면, Interpreter.call()에서 전파된 예외는 ExecutionFailed를 거치지 않고, 마치 호출한 인터프리터에서 발생한 것처럼 직접 발생합니다. Interpreter.call()은 일반적으로 인터프리터 간에 전달할 수 없는 객체를 지원하기 위해 pickle을 사용하는 더 고수준의 메서드이기 때문입니다.
객체 대 ID 프록시
인터프리터와 큐 모두에 대해, 저수준 모듈은 기반 상태를 그에 대응하는 프로세스 전역 ID로 노출하는 프록시 객체를 사용합니다. 두 경우 모두 상태 역시 프로세스 전역이며 여러 인터프리터에서 사용됩니다. 따라서 이들은 PyObject로 구현하기에 적합하지 않으며, PyObject는 사실상 인터프리터별 데이터에만 적합한 선택지입니다. 그래서 interpreters 모듈은 그 대신 ID를 통해 약하게 연결된 객체를 제공합니다.
기각된 아이디어
PEP 554를 참고하십시오.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.