Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 554 – 표준 라이브러리의 여러 인터프리터

Author:
Eric Snow <ericsnowcurrently at gmail.com>
Discussions-To:
Discourse thread
Status:
Superseded
Type:
Standards Track
Created:
05-Sep-2017
Python-Version:
3.13
Post-History:
07-Sep-2017, 08-Sep-2017, 13-Sep-2017, 05-Dec-2017, 04-May-2020, 14-Mar-2023, 01-Nov-2023
Superseded-By:
734

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Note

이 PEP는 사실상 PEP 734에서 더 깔끔한 형태로 이어집니다. 이 PEP는 PEP 734에서 제거된 배경 정보와 연기되거나 거부된 아이디어에 관한 여러 섹션을 위해 현재 형태 그대로 유지합니다.

초록

CPython은 버전 1.5(1997년)부터 동일한 프로세스에서 여러 인터프리터(즉, “서브인터프리터”)를 지원해 왔습니다. 이 기능은 C-API를 통해 사용할 수 있었습니다. [c-api] 여러 인터프리터는 서로 상대적으로 격리된 상태로 작동하며, 이는 동시성에 대한 새롭고 대안적인 접근 방식을 가능하게 합니다.

이 제안은 표준 라이브러리 interpreters 모듈을 도입합니다. 이 모듈은 C-API에서 이미 제공하는 여러 인터프리터의 기본 기능과 인터프리터 간 통신을 위한 기본 지원을 공개합니다. Python 3.12에서 인터프리터별 GIL을 도입한 PEP 684 이후 이 모듈은 특히 중요합니다.

제안

요약:

  • 새로운 표준 라이브러리 모듈 추가: “interpreters”
  • concurrent.futures.InterpreterPoolExecutor 추가
  • 확장 모듈 유지 관리자 지원

“interpreters” 모듈

interpreters 모듈은 여러 인터프리터 기능에 대한 고수준 인터페이스를 제공하고, 새로운 저수준 _interpreters를 래핑합니다(threading 모듈과 동일한 방식입니다). 구체적인 사용법과 사용 사례는 Examples 섹션을 참조하십시오.

이 모듈은 CPython에 기존하는 여러 인터프리터 지원을 공개하는 것과 함께, 인터프리터 간에 데이터를 전달하기 위한 기본 메커니즘도 지원합니다. 여기에는 대상 서브인터프리터의 __main__ 모듈에 “공유 가능한” 객체를 설정하는 작업이 포함됩니다. os.pipe()와 같은 일부 객체는 추가 통신에 사용될 수 있습니다. 또한 이 모듈은 인터프리터 간 통신을 시연하기 위한 “채널”의 최소 구현을 제공합니다.

objects는 생성된 인터프리터에 연결되어 있으므로 인터프리터 간에 공유되지 않는다는 점에 유의하십시오. 대신 객체의 data가 인터프리터 간에 전달됩니다. 인터프리터 간 공유 및 통신에 관한 자세한 내용은 Shared DataAPI For Communication 섹션을 참조하십시오.

interpreters 모듈 API 요약

다음은 interpreters 모듈 API의 요약입니다. 제안된 클래스와 함수에 대한 자세한 설명은 아래의 “interpreters” Module API 섹션을 참조하십시오.

인터프리터 생성 및 사용:

시그니처 설명
list_all() -> [Interpreter] 존재하는 모든 인터프리터를 가져옵니다.
get_current() -> Interpreter 현재 실행 중인 인터프리터를 가져옵니다.
get_main() -> Interpreter 주 인터프리터를 가져옵니다.
create() -> Interpreter 새로운 (유휴) Python 인터프리터를 초기화합니다.

서명 설명
class Interpreter 단일 인터프리터입니다.
.id 인터프리터의 ID입니다 (읽기 전용).
.is_running() -> bool 현재 인터프리터가 코드를 실행하고 있습니까?
.close() 인터프리터를 종료하고 제거합니다.
.set_main_attrs(**kwargs) __main__에 “공유 가능한” 객체를 바인딩합니다.
.get_main_attr(name) __main__에서 “공유 가능한” 객체를 가져옵니다.
.exec(src_str, /)
주어진 소스 코드를 인터프리터에서 실행합니다
(현재 스레드에서).

인터프리터 간 통신:

서명 설명
is_shareable(obj) -> Bool
객체의 데이터를 인터프리터 간에 전달할
수 있습니까?
create_channel() -> (RecvChannel, SendChannel)
인터프리터 간에 데이터를 전달하기 위한
새 채널을 만듭니다.

concurrent.futures.InterpreterPoolExecutor

스레드별 작업을 서브인터프리터에서 실행하도록 ThreadPoolExecutor를 확장한 실행기가 추가됩니다. 처음에는 Interpreter.exec()가 받는 것(예: str 스크립트)만 지원되는 작업이 될 것입니다. 그러나 단기 실행 작업의 성능을 희생하여 불편을 줄이기 위해 일부 함수도 지원하고, 나중에는 작업과 인자를 피클링하는 별도의 메서드도 지원할 수 있습니다.

확장 모듈 유지 관리자를 위한 도움말

실제로 다중 단계 init (PEP 489)을 구현하는 확장은 격리된 것으로 간주되므로 여러 인터프리터와 호환됩니다. 그렇지 않으면 “호환되지 않습니다.”입니다.

많은 확장 모듈이 여전히 호환되지 않습니다. 이러한 확장 모듈의 유지 관리자와 사용자는 해당 모듈이 업데이트되어 여러 인터프리터를 지원하게 되면 모두 혜택을 얻습니다. 그동안 사용자는 여러 인터프리터를 사용할 때 발생하는 실패로 혼란을 겪을 수 있으며, 이는 확장 모듈 유지 관리자에게 부정적인 영향을 줄 수 있습니다. 아래의 Concerns를 참조하십시오.

이러한 영향을 완화하고 호환성을 높이기 위해 다음을 수행합니다:

  • 확장 모듈이 여러 인터프리터에서 사용되는 것을 지원할 필요가 없음을 명확히 합니다.
  • 호환되지 않는 모듈을 서브인터프리터에서 가져올 때 ImportError를 발생시킵니다.
  • 유지 관리자가 호환성을 달성하는 데 도움이 되는 리소스(예: 문서)를 제공합니다.
  • 피드백을 받고 가능하다면 지원을 제공하기 위해 Cython의 유지 관리자와 PyPI에서 가장 많이 사용되는 확장 모듈의 유지 관리자에게 연락합니다.

예제

현재 OS 스레드에서 격리된 코드 실행

interp = interpreters.create()
print('before')
interp.exec('print("during")')
print('after')

다른 스레드에서 실행

interp = interpreters.create()
def run():
    interp.exec('print("during")')
t = threading.Thread(target=run)
print('before')
t.start()
t.join()
print('after')

인터프리터 미리 채우기

interp = interpreters.create()
interp.exec(tw.dedent("""
    import some_lib
    import an_expensive_module
    some_lib.set_up()
    """))
wait_for_request()
interp.exec(tw.dedent("""
    some_lib.handle_request()
    """))

예외 처리

interp = interpreters.create()
try:
    interp.exec(tw.dedent("""
        raise KeyError
        """))
except interpreters.RunFailedError as exc:
    print(f"got the error from the subinterpreter: {exc}")

예외 다시 발생시키기

interp = interpreters.create()
try:
    try:
        interp.exec(tw.dedent("""
            raise KeyError
            """))
    except interpreters.RunFailedError as exc:
        raise exc.__cause__
except KeyError:
    print("got a KeyError from the subinterpreter")

이 패턴은 나중에 개선할 후보임에 유의하십시오.

__main__ 네임스페이스와 상호 작용

interp = interpreters.create()
interp.set_main_attrs(a=1, b=2)
interp.exec(tw.dedent("""
    res = do_something(a, b)
    """))
res = interp.get_main_attr('res')

OS 파이프를 사용하여 동기화

interp = interpreters.create()
r1, s1 = os.pipe()
r2, s2 = os.pipe()

def task():
    interp.exec(tw.dedent(f"""
        import os
        os.read({r1}, 1)
        print('during B')
        os.write({s2}, '')
        """))

t = threading.thread(target=task)
t.start()
print('before')
os.write(s1, '')
print('during A')
os.read(r2, 1)
print('after')
t.join()

파일 디스크립터 공유

interp = interpreters.create()
with open('spamspamspam') as infile:
    interp.set_main_attrs(fd=infile.fileno())
    interp.exec(tw.dedent(f"""
        import os
        for line in os.fdopen(fd):
            print(line)
        """))

피클을 통해 객체 전달

interp = interpreters.create()
r, s = os.pipe()
interp.exec(tw.dedent(f"""
    import os
    import pickle
    reader = {r}
    """))
interp.exec(tw.dedent("""
        data = b''
        c = os.read(reader, 1)
        while c != b'\x00':
            while c != b'\x00':
                data += c
                c = os.read(reader, 1)
            obj = pickle.loads(data)
            do_something(obj)
            c = os.read(reader, 1)
        """))
for obj in input:
    data = pickle.dumps(obj)
    os.write(s, data)
    os.write(s, b'\x00')
os.write(s, b'\x00')

인터프리터의 stdout 캡처

interp = interpreters.create()
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
    interp.exec(tw.dedent("""
        print('spam!')
        """))
assert(stdout.getvalue() == 'spam!')

# alternately:
interp.exec(tw.dedent("""
    import contextlib, io
    stdout = io.StringIO()
    with contextlib.redirect_stdout(stdout):
        print('spam!')
    captured = stdout.getvalue()
    """))
captured = interp.get_main_attr('captured')
assert(captured == 'spam!')

파이프(os.pipe())도 유사하게 사용할 수 있습니다.

모듈 실행

interp = interpreters.create()
main_module = mod_name
interp.exec(f'import runpy; runpy.run_module({main_module!r})')

스크립트로 실행(압축 아카이브 및 디렉터리 포함)

interp = interpreters.create()
main_script = path_name
interp.exec(f"import runpy; runpy.run_path({main_script!r})")

통신을 위해 채널 사용

tasks_recv, tasks = interpreters.create_channel()
results, results_send = interpreters.create_channel()

def worker():
    interp = interpreters.create()
    interp.set_main_attrs(tasks=tasks_recv, results=results_send)
    interp.exec(tw.dedent("""
        def handle_request(req):
            ...

        def capture_exception(exc):
            ...

        while True:
            try:
                req = tasks.recv()
            except Exception:
                # channel closed
                break
            try:
                res = handle_request(req)
            except Exception as exc:
                res = capture_exception(exc)
            results.send_nowait(res)
        """))
threads = [threading.Thread(target=worker) for _ in range(20)]
for t in threads:
    t.start()

requests = ...
for req in requests:
    tasks.send(req)
tasks.close()

for t in threads:
    t.join()

메모리 뷰 공유(맵리듀스를 가정)

data, chunksize = read_large_data_set()
buf = memoryview(data)
numchunks = (len(buf) + 1) / chunksize
results = memoryview(b'\0' * numchunks)

tasks_recv, tasks = interpreters.create_channel()

def worker():
    interp = interpreters.create()
    interp.set_main_attrs(data=buf, results=results, tasks=tasks_recv)
    interp.exec(tw.dedent("""
        while True:
            try:
                req = tasks.recv()
            except Exception:
                # channel closed
                break
            resindex, start, end = req
            chunk = data[start: end]
            res = reduce_chunk(chunk)
            results[resindex] = res
        """))
t = threading.Thread(target=worker)
t.start()

for i in range(numchunks):
    if not workers_running():
        raise ...
    start = i * chunksize
    end = start + chunksize
    if end > len(buf):
        end = len(buf)
    tasks.send((start, end, i))
tasks.close()
t.join()

use_results(results)

근거

동일한 프로세스 내에서 여러 인터프리터로 코드를 실행하면 유용한 수준의 격리를 제공합니다. 이를 여러 방식으로 활용할 수 있습니다. 또한 서브인터프리터는 이러한 격리를 확장할 수 있는 잘 정의된 프레임워크를 제공합니다. ( PEP 684를 참조하십시오.)

Alyssa (Nick) Coghlan은 다중 처리와 비교하여 그 이점 중 일부를 설명했습니다 [benefits]:

[I] expect that communicating between subinterpreters is going
to end up looking an awful lot like communicating between
subprocesses via shared memory.

The trade-off between the two models will then be that one still
just looks like a single process from the point of view of the
outside world, and hence doesn't place any extra demands on the
underlying OS beyond those required to run CPython with a single
interpreter, while the other gives much stricter isolation
(including isolating C globals in extension modules), but also
demands much more from the OS when it comes to its IPC
capabilities.

The security risk profiles of the two approaches will also be quite
different, since using subinterpreters won't require deliberately
poking holes in the process isolation that operating systems give
you by default.

CPython은 버전 1.5부터 지원 수준을 높여 가며 여러 인터프리터를 지원해 왔습니다. 이 기능은 강력한 도구가 될 가능성이 있지만, 여러 인터프리터 기능을 Python에서 직접 쉽게 사용할 수 없었기 때문에 방치되어 왔습니다. 기존 기능을 표준 라이브러리에 노출하면 이러한 상황을 개선하는 데 도움이 됩니다.

이 제안은 동일한 Python 프로세스에서 서로 격리된 여러 인터프리터라는 기본 기능을 활성화하는 데 중점을 둡니다. 이는 Python에 새로운 영역이므로 인터프리터와 함께 제공할 최적의 도구가 무엇인지에 대해 상대적으로 불확실합니다. 따라서 이 제안에서 추가하는 기능은 가능한 한 최소화합니다.

우려 사항

  • “서브인터프리터는 수고를 들일 가치가 없습니다”

일부에서는 서브인터프리터가 Python의 공식 구성 요소로 만들 만큼 충분한 이점을 제공하지 않는다고 주장했습니다. 언어(또는 표준 라이브러리)에 기능을 추가하면 언어의 규모가 커지는 비용이 발생합니다. 따라서 추가 기능은 스스로 비용을 상쇄해야 합니다.

이 경우 여러 인터프리터 지원은 격리된 실행 스레드에 중점을 둔 새로운 동시성 모델을 제공합니다. 또한 이는 CPython을 변경하여 여러 CPU 코어를 동시에 사용할 수 있게 할 기회를 제공합니다(현재는 GIL로 인해 방지되고 있습니다–PEP 684를 참조하십시오).

서브인터프리터의 대안으로는 스레딩, async 및 다중 처리가 있습니다. 스레딩은 GIL에 의해 제한되며 async는 모든 문제에 적합한 해결책이 아니며 모든 사람에게 적합한 것도 아닙니다. 다중 처리 역시 일부 상황에서는 유용하지만 모든 상황에서 유용한 것은 아닙니다. 직접 IPC(다중 처리 모듈을 통하지 않는 방식)도 유사한 이점을 제공하지만 동일한 단점이 있습니다.

특히 서브인터프리터는 위의 어떤 기능도 대체하기 위한 것이 아닙니다. 물론 일부 영역에서는 서로 겹치지만, 서브인터프리터의 이점에는 격리와 (잠재적인) 성능이 포함됩니다. 특히 서브인터프리터는 다른 곳에서 성공을 거두었으며 일부 Python 사용자에게 매력적으로 다가갈 대체 동시성 모델(예: CSP)로 가는 직접적인 경로를 제공합니다. 이것이 바로 interpreters 모듈이 제공할 핵심 가치입니다.

  • “여러 인터프리터에 대한 표준 라이브러리 지원은 C 확장 작성자에게 추가 부담을 줍니다”

아래의 Interpreter Isolation 섹션에서는 CPython의 서브인터프리터에서 격리가 불완전한 방식을 식별합니다. 가장 주목할 만한 것은 내부 상태를 저장하는 데 C 전역 변수를 사용하는 확장 모듈입니다. (PEP 3121PEP 489는 이 문제에 대한 해결책을 제공하며, 이어서 효율성을 개선하는 몇 가지 추가 API가 제공됩니다(예: PEP 573).)

결과적으로 확장 모듈을 배포하는 프로젝트는 사용자가 서브인터프리터를 사용하기 시작하면서 유지 관리 부담이 증가할 수 있으며, 이때 해당 모듈이 손상될 수 있습니다. 이러한 상황은 내부 상태를 저장하는 데 C 전역 변수를 사용하는 모듈(또는 C 전역 변수를 사용하는 라이브러리를 사용하는 모듈)에만 해당합니다. numpy의 경우 보고된 버그 발생률은 6개월에 한 건입니다. [bug-rate]

궁극적으로 이는 실제로 얼마나 자주 문제가 될 것인지에 관한 질문으로 귀결됩니다. 영향을 받을 프로젝트의 수, 해당 프로젝트 사용자들이 영향을 받을 빈도, 프로젝트에 추가로 발생할 유지 관리 부담, 그리고 그러한 비용을 상쇄할 서브인터프리터의 전반적인 이점이 무엇인지가 문제입니다. 이 PEP의 입장은 실제 추가 유지 관리 부담이 작으며, 서브인터프리터를 사용할 가치가 있다고 판단할 기준보다 훨씬 낮을 것이라는 것입니다.

  • “새로운 동시성 API를 만드는 일은 훨씬 더 많은 숙고와 실험을 필요로 하므로, 새 모듈은 설령 포함되더라도 곧바로 표준 라이브러리에 들어가서는 안 됩니다”

asyncio에서 그랬던 것처럼 새로운 동시성 모델을 위한 API를 도입하는 일은 매우 큰 프로젝트이며, 많은 신중한 검토가 필요합니다. 이는 이 PEP가 제안하는 것처럼 단순하게 수행할 수 있는 일이 아니며, 성숙해지기까지 PyPI에서 상당한 시간을 필요로 할 가능성이 큽니다. (python-dev에 게시된 Nathaniel’s post를 참조하십시오.)

그러나 이 PEP는 새로운 동시성 API를 제안하지 않습니다. 기껏해야 (비교적) Python에 새롭게 등장한 concurrency models과 관련된 패턴을 따르는 코드를 작성하는 데 사용할 수 있는 최소한의 도구(예: 서브인터프리터, 채널)만 노출합니다. 또한 이러한 도구를 그러한 동시성 모델을 위한 API의 기반으로 사용할 수도 있습니다. 다시 말하지만, 이 PEP는 그러한 API를 제안하지 않습니다.

  • “여전히 GIL을 공유한다면 서브인터프리터를 노출해도 의미가 없습니다”
  • “GIL을 인터프리터별로 만들려는 작업은 큰 혼란을 일으키며 위험합니다”

흔히 있는 오해는 이 PEP가 인터프리터들이 더 이상 GIL을 공유하지 않을 것이라는 약속도 포함한다고 생각하는 것입니다. 이를 명확히 하면 다음 질문은 “무슨 의미가 있습니까?”가 됩니다. 이에 대한 답변은 이미 이 PEP에서 자세히 제시했습니다. 분명히 하자면, 그 가치는 다음에 있습니다.:

* increase exposure of the existing feature, which helps improve
  the code health of the entire CPython runtime
* expose the (mostly) isolated execution of interpreters
* preparation for per-interpreter GIL
* encourage experimentation
  • “데이터 공유는 멀티코어 시나리오에서 캐시 성능에 부정적인 영향을 줄 수 있습니다”

([cache-line-ping-pong]을 참조하십시오.)

실제로 인터프리터 간에 데이터를 공유할 즉각적인 계획은 없고 복사에 집중하고 있으므로, 지금은 이것이 문제가 되지 않아야 합니다.

서브인터프리터에 관하여

동시성

동시성은 소프트웨어 개발에서 어려운 영역입니다. 수십 년에 걸친 연구와 실무를 통해 서로 다른 목표를 가진 매우 다양한 동시성 모델이 등장했습니다. 대부분은 정확성과 사용 편의성을 중심으로 합니다.

동시성 모델의 한 부류는 어떤 메시지 전달 방식을 통해 상호 운용하는 격리된 실행 스레드에 초점을 맞춥니다. 주목할 만한 예로 Communicating Sequential Processes [CSP]가 있습니다(Go의 동시성은 대략적으로 이를 기반으로 합니다). CPython 인터프리터에 내재된 의도된 격리 특성은 인터프리터를 이 접근 방식에 매우 적합하게 만듭니다.

공유 데이터

CPython의 인터프리터는 스레드와 달리 본질적으로 격리되어 있습니다(아래에서 설명하는 단서가 있습니다). 따라서 동일한 공유 메모리를 통한 통신 접근 방식은 작동하지 않습니다. 대안이 없다면 여러 인터프리터를 통한 동시성의 효과적인 활용은 크게 제한됩니다.

여기서 핵심 과제는 객체 소유권, 가시성 및 변경 가능성에 대한 다양한 제약으로 인해 인터프리터 간 객체 공유가 복잡해진다는 점입니다. 개념적으로는 객체가 한 번에 하나의 인터프리터에만 존재할 때 동시성을 추론하기가 더 쉽습니다. 기술적으로는 CPython의 현재 메모리 모델이 Python 객체를 인터프리터 간에 안전하게 공유하는 방법을 제한합니다. 사실상 객체는 자신이 생성된 인터프리터에 속박됩니다. 또한 인터프리터가 더욱 격리될수록 객체공유의 복잡성은 증가합니다. 예를 들어 GIL 제거 이후가 이에 해당합니다(다만 일부 “불멸” 객체의 경우에는 이러한 영향이 어느 정도 완화됩니다( PEP 683 참조)).

따라서 공유 메커니즘은 신중하게 고려해야 합니다. 유효한 해결책은 여러 가지가 있으며, 그중 일부는 Python의 표준 라이브러리와 C-API에서 지원하기에 적절할 수 있습니다. 그러한 해결책은 어느 것이든 다른 해결책과 많은 특성을 공유할 가능성이 높습니다.

그동안 여기서는 객체 공유 방식에 대한 선례를 제시하는 최소한의 해결책(Interpreter.set_main_attrs())을 제안합니다. 더 중요한 점은 이 해결책이 이후에 더욱 고급 접근법을 도입하기 쉽게 하고, 그러한 접근법들이 공존하며 협력할 수 있도록 한다는 것입니다. 이를 부분적으로 보여 주기 위해, 다소 더 고급인 공유 해결책으로서 “채널”의 기본 구현을 제공합니다.

별도의 제안에서 다룰 수 있는 내용은 다음과 같습니다:

  • Interpreter.set_main_attrs()의 구현을 기반으로 한 공개 C-API의 추가
  • “interpreters” 모듈에 다른 공유 접근법을 추가하는 것

통신을 가능하게 하는 근본적인 기능은 대부분의 객체를 인터프리터 간에 전달해도 안전한 기초 원시 데이터의 어떤 인코딩으로 변환할 수 있다는 점입니다. 예를 들어 int 객체를 C long 값으로 변환하여 다른 인터프리터로 보내고, 그곳에서 다시 int 객체로 변환할 수 있습니다. 또 다른 예로, None은 있는 그대로 전달할 수 있습니다.

어느 경우든 여기서 최선의 방향을 결정하기 위한 노력은 대부분 이 PEP의 범위를 벗어납니다. 그동안 이 제안에서는 파이프(os.pipe())를 사용하는 기본적인 임시 해결책을 설명하고, 전용 기능(“채널”)도 제공합니다. 아래의 API For Communication 을 참조하십시오.

인터프리터 격리

CPython의 인터프리터는 서로 엄격하게 격리되도록 설계되었습니다. 각 인터프리터는 모든 모듈, 클래스, 함수 및 변수의 자체 복사본을 가집니다. C의 상태에도 동일하게 적용되며, 여기에는 확장 모듈의 상태도 포함됩니다. CPython C-API 문서에서 자세한 내용을 설명합니다. [caveats]

그러나 인터프리터가 일부 상태를 공유하는 방식도 있습니다. 우선 일부 프로세스 전역 상태는 여전히 공유됩니다.

  • 파일 디스크립터
  • 저수준 환경 변수
  • 프로세스 메모리(단, 할당자는 격리되어 있습니다)
  • 내장 타입(예: dict, bytes)
  • 싱글턴(예: None)
  • 내장/확장/동결 모듈의 기본 정적 모듈 데이터(예: 함수)

이를 변경할 계획은 없습니다.

둘째, 서브인터프리터를 고려하지 않은 버그나 구현으로 인해 일부 격리가 제대로 이루어지지 않습니다. 여기에는 C 전역 변수에 의존하는 확장 모듈과 같은 것들이 포함됩니다. [cryptography] 이러한 경우에는 버그를 제보해야 합니다(이미 제보된 것도 있습니다).

마지막으로, CPython의 현재 설계로 인해 일부 잠재적인 격리가 누락되어 있습니다. 이 영역의 공백을 해결하기 위한 개선 작업이 현재 진행 중입니다.

  • PyGILState_* API를 사용하는 확장은 어느 정도 호환되지 않습니다 [gilstate]

기존 사용

다중 인터프리터 지원은 널리 사용되는 기능이 아니었습니다. 실제로 널리 사용된 사례로 문서화된 것은 극히 일부에 불과하며, 여기에는 mod_wsgi, OpenStack Ceph, JEP가 포함됩니다. 한편으로 이러한 사례는 기존 다중 인터프리터 지원이 비교적 안정적이라는 확신을 제공합니다. 다른 한편으로는 이 기능의 유용성을 판단할 수 있을 만큼 표본 크기가 크지 않습니다.

대체 Python 구현

저는 여러 Python 구현자에게 서브인터프리터 지원에 관한 의견을 요청했습니다. 각 구현자는 원한다면 큰 어려움 없이 동일한 프로세스에서 다중 인터프리터를 지원할 수 있다고 밝혔습니다. 제가 연락한 프로젝트는 다음과 같습니다.

  • jython ([jython])
  • ironpython (개인 서신)
  • pypy (개인 서신)
  • micropython (개인 서신)

“interpreters” 모듈 API

이 모듈은 다음 함수를 제공합니다.:

list_all() -> [Interpreter]

   Return a list of all existing interpreters.

get_current() => Interpreter

   Return the currently running interpreter.

get_main() => Interpreter

   Return the main interpreter.  If the Python implementation
   has no concept of a main interpreter then return None.

create() -> Interpreter

   Initialize a new Python interpreter and return it.
   It will remain idle until something is run in it and always
   run in its own thread.

is_shareable(obj) -> bool:

   Return True if the object may be "shared" between interpreters.
   This does not necessarily mean that the actual objects will be
   shared.  Instead, it means that the objects' underlying data will
   be shared in a cross-interpreter way, whether via a proxy, a
   copy, or some other means.

이 모듈은 다음 클래스도 제공합니다.:

class Interpreter(id):

   id -> int:

      The interpreter's ID. (read-only)

   is_running() -> bool:

      Return whether or not the interpreter's "exec()" is currently
      executing code.  Code running in subthreads is ignored.
      Calling this on the current interpreter will always return True.

   close():

      Finalize and destroy the interpreter.

      This may not be called on an already running interpreter.
      Doing so results in a RuntimeError.

   set_main_attrs(iterable_or_mapping, /):
   set_main_attrs(**kwargs):

      Set attributes in the interpreter's __main__ module
      corresponding to the given name-value pairs.  Each value
      must be a "shareable" object and will be converted to a new
      object (e.g. copy, proxy) in whatever way that object's type
      defines.  If an attribute with the same name is already set,
      it will be overwritten.

      This method is helpful for setting up an interpreter before
      calling exec().

   get_main_attr(name, default=None, /):

      Return the value of the corresponding attribute of the
      interpreter's __main__ module.  If the attribute isn't set
      then the default is returned.  If it is set, but the value
      isn't "shareable" then a ValueError is raised.

      This may be used to introspect the __main__ module, as well
      as a very basic mechanism for "returning" one or more results
      from Interpreter.exec().

   exec(source_str, /):

      Run the provided Python source code in the interpreter,
      in its __main__ module.

      This may not be called on an already running interpreter.
      Doing so results in a RuntimeError.

      An "interp.exec()" call is similar to a builtin exec() call
      (or to calling a function that returns None).  Once
      "interp.exec()" completes, the code that called "exec()"
      continues executing (in the original interpreter).  Likewise,
      if there is any uncaught exception then it effectively
      (see below) propagates into the code where ``interp.exec()``
      was called.  Like exec() (and threads), but unlike function
      calls, there is no return value.  If any "return" value from
      the code is needed, send the data out via a pipe (os.pipe())
      or channel or other cross-interpreter communication mechanism.

      The big difference from exec() or functions is that
      "interp.exec()" executes the code in an entirely different
      interpreter, with entirely separate state.  The interpreters
      are completely isolated from each other, so the state of the
      original interpreter (including the code it was executing in
      the current OS thread) does not affect the state of the target
      interpreter (the one that will execute the code).  Likewise,
      the target does not affect the original, nor any of its other
      threads.

      Instead, the state of the original interpreter (for this thread)
      is frozen, and the code it's executing code completely blocks.
      At that point, the target interpreter is given control of the
      OS thread.  Then, when it finishes executing, the original
      interpreter gets control back and continues executing.

      So calling "interp.exec()" will effectively cause the current
      Python thread to completely pause.  Sometimes you won't want
      that pause, in which case you should make the "exec()" call in
      another thread.  To do so, add a function that calls
      "interp.exec()" and then run that function in a normal
      "threading.Thread".

      Note that the interpreter's state is never reset, neither
      before "interp.exec()" executes the code nor after.  Thus the
      interpreter state is preserved between calls to
      "interp.exec()".  This includes "sys.modules", the "builtins"
      module, and the internal state of C extension modules.

      Also note that "interp.exec()" executes in the namespace of the
      "__main__" module, just like scripts, the REPL, "-m", and
      "-c".  Just as the interpreter's state is not ever reset, the
      "__main__" module is never reset.  You can imagine
      concatenating the code from each "interp.exec()" call into one
      long script.  This is the same as how the REPL operates.

      Supported code: source text.

Interpreter.set_main_attrs()의 기능 외에도 이 모듈은 인터프리터 간에 데이터를 전달하는 관련 방법인 채널을 제공합니다. 아래의 Channels를 참조하십시오.

포착되지 않은 예외

Interpreter.exec()에서 발생한 포착되지 않은 예외와 관련하여, 해당 예외가 interp.exec()가 호출된 코드로 사실상 전파된다는 점을 확인했습니다. 인터프리터 간에 예외와 트레이스백이 누출되는 것을 방지하기 위해, 예외와 해당 트레이스백의 대리 객체(traceback.TracebackException 참조)를 생성하고, 새 interpreters.RunFailedError__cause__에 이를 설정한 다음 이를 발생시킵니다.

예외의 프록시를 직접 발생시키는 방식은 interp.exec() 호출에서 발생한 오류와 서브인터프리터에서 발생한 포착되지 않은 예외를 구분하기가 더 어렵기 때문에 문제가 됩니다.

인터프리터 제한 사항

interpreters.create()로 생성되는 모든 새 인터프리터에는 이제 실행하는 모든 코드에 적용되는 특정 제한이 있습니다. 여기에는 다음이 포함됩니다.

  • 다중 단계 초기화를 구현하지 않은 경우 확장 모듈을 가져오지 못합니다.
  • 데몬 스레드를 생성할 수 없습니다.
  • os.fork()를 사용할 수 없습니다(따라서 multiprocessing도 사용할 수 없습니다).
  • os.exec*()를 사용할 수 없습니다(하지만 subprocess와 같은 “fork+exec” 방식은 괜찮습니다).

기존 C-API로 생성된 인터프리터에는 이러한 제한이 없다는 점에 유의하십시오. “주” 인터프리터도 마찬가지이므로 기존 Python 사용 방식은 변경되지 않습니다.

나중에 위 제한 중 일부를 완화하거나 세부 제한을 개별적으로 활성화하거나 비활성화하는 방법을 제공할 수도 있습니다. 그와 관계없이 확장 모듈에 다중 단계 초기화를 요구하는 것은 항상 기본 제한으로 유지됩니다.

통신을 위한 API

위의 Shared Data 에서 설명했듯이, 인터프리터 간에 데이터를 공유(통신)하는 메커니즘이 없으면 다중 인터프리터 지원은 덜 유용합니다. 그러나 실제 Python 객체를 인터프리터 간에 공유하면 잠재적인 문제가 충분히 발생할 수 있으므로, 이 제안에서는 이에 대한 지원을 피하고 있습니다. 또한 앞서 언급했듯이 기본적인 통신 메커니즘을 넘어서는 기능은 추가하지 않습니다.

해당 메커니즘은 Interpreter.set_main_attrs() 메서드입니다. Interpreter.exec()가 호출되기 전에 전역 변수를 설정하는 데 사용할 수 있습니다. set_main_attrs()에 전달된 이름-값 쌍은 인터프리터의 __main__ 모듈 속성으로 바인딩됩니다. 값은 “shareable”이어야 합니다. 아래의 Shareable Types 를 참조하십시오.

Interpreter.set_main_attrs()를 통해 객체를 통신하고 공유하는 추가 방법을 사용할 수 있습니다. 큐처럼 작동하면서 인터프리터 간 안전성을 제공하는 공유 가능한 객체를 구현할 수 있습니다. 실제로 이 PEP에는 이러한 접근 방식의 예인 채널이 포함되어 있습니다.

공유 가능한 타입

객체의 타입이 공유 가능한 인스턴스를 지원하면 해당 객체는 “shareable”입니다. 해당 타입은 객체를 인터프리터 독립적 데이터로 변환한 다음 다른 쪽에서 다시 객체로 변환하는 데 사용되는 새로운 내부 프로토콜을 구현해야 합니다. 위의 is_shareable() 도 참조하십시오.

처음에는 다음을 포함하여 단순하고 변경 불가능한 최소한의 내장 타입 집합을 지원합니다.

  • None
  • bool
  • bytes
  • str
  • int
  • float

초기에는 소수의 복합 타입도 지원합니다:

  • memoryview, PEP 3118 버퍼를 공유할 수 있도록 합니다.
  • 채널

이후에는 복합 타입인지 여부와 관계없이 추가 내장 타입을 지원할 수 있습니다. 초기 공유 가능 타입을 제한하는 것은 실용적인 문제로, 초기 구현의 잠재적인 복잡성을 줄입니다. 인터프리터 격리에 관해 더 많은 경험을 쌓으면, 향후 지원 객체를 확장하기 위해 여러 전략을 추진할 수 있습니다.

그동안 별도의 제안에서는 Interpreter.set_main_attrs()에서 사용하는 내부 프로토콜(및 C-API)을 공개하는 방안을 논의합니다. 해당 프로토콜을 사용하면 확장 모듈에서 다른 타입에 대한 지원을 추가할 수 있습니다.

OS 파이프를 통한 통신

통신을 위한 전용 객체가 없어도 사용자는 이미 존재하는 도구를 사용할 수 있습니다. 예를 들어 인터프리터 간에 데이터를 전송하는 한 가지 기본적인 방법은 파이프를 사용하는 것입니다(os.pipe() 참조):

  1. 인터프리터 A가 os.pipe()를 호출하여 파일 디스크립터로 구성된 읽기/쓰기 쌍(둘 다 int 객체)을 가져옵니다.
  2. 인터프리터 A가 interp.set_main_attrs()를 호출하여 읽기 FD를 바인딩합니다(또는 문자열 포매팅을 사용해 삽입합니다).
  3. 인터프리터 A가 인터프리터 B에서 interp.exec()를 호출합니다.
  4. 인터프리터 A가 쓰기 FD에 일부 바이트를 기록합니다.
  5. 인터프리터 B가 해당 바이트를 읽습니다.

앞서 나온 여러 예제는 Synchronize using an OS pipe와 같은 방식으로 이를 보여 줍니다.

채널

interpreters 모듈에는 인터프리터 간에 객체 데이터를 전달하기 위한 전용 솔루션인 채널이 포함됩니다. 채널은 한편으로는 os.pipe()를 사용하는 것보다 더 쉬운 메커니즘을 제공하고, 다른 한편으로는 라이브러리에서 Interpreter.set_main_attrs()를 비롯하여 해당 메서드가 사용하는 프로토콜을 활용하는 방법을 보여 주기 위해 모듈에 포함됩니다.

채널은 단방향 FIFO입니다. 채널은 파이프, 큐 및 CSP의 채널에서 영감을 얻은 기본적인 선택적 데이터 공유 메커니즘입니다. [fifo] 파이프와의 주요 차이점은 채널의 양쪽 끝을 0개 이상의 인터프리터와 연결할 수 있다는 점입니다. 마찬가지로 다대다 방식인 큐와 같이 채널은 버퍼링되지만, 버퍼링되지 않는 의미론을 갖는 메서드도 제공합니다.

채널에는 두 가지 연산인 전송과 수신이 있습니다. 이러한 연산의 주요 특징은 채널이 객체 자체가 아니라 Python 객체에서 파생된 데이터를 전송한다는 점입니다. 객체를 전송하면 해당 객체의 데이터가 추출됩니다. 다른 인터프리터에서 “객체”를 수신하면 데이터가 해당 인터프리터가 소유하는 객체로 다시 변환됩니다.

이를 구현하기 위해 변경 가능한 공유 상태는 어떤 인터프리터도 아닌 Python 런타임이 관리합니다. 초기에는 공유 상태에 대해 한 가지 객체 유형만 지원합니다. 바로 interpreters.create_channel()에서 제공하는 채널입니다. 그러면 채널이 인터프리터 간 객체 전달을 신중하게 관리합니다.

API를 최소한으로 유지하는 것을 포함한 이 접근 방식은 Python 사용자에게 내부의 복잡성을 더 이상 노출하지 않도록 도와줍니다.

interpreters 모듈은 채널과 관련된 다음 함수를 제공합니다.:

create_channel() -> (RecvChannel, SendChannel):

   Create a new channel and return (recv, send), the RecvChannel
   and SendChannel corresponding to the ends of the channel.

   Both ends of the channel are supported "shared" objects (i.e.
   may be safely shared by different interpreters.  Thus they
   may be set using "Interpreter.set_main_attrs()".

이 모듈은 채널과 관련된 다음 클래스도 제공합니다.:

class RecvChannel(id):

   The receiving end of a channel.  An interpreter may use this to
   receive objects from another interpreter.  Any type supported by
   Interpreter.set_main_attrs() will be supported here, though at
   first only a few of the simple, immutable builtin types
   will be supported.

   id -> int:

      The channel's unique ID.  The "send" end has the same one.

   recv(*, timeout=None):

      Return the next object from the channel.  If none have been
      sent then wait until the next send (or until the timeout is hit).

      At the least, the object will be equivalent to the sent object.
      That will almost always mean the same type with the same data,
      though it could also be a compatible proxy.  Regardless, it may
      use a copy of that data or actually share the data.  That's up
      to the object's type.

   recv_nowait(default=None):

      Return the next object from the channel.  If none have been
      sent then return the default.  Otherwise, this is the same
      as the "recv()" method.


class SendChannel(id):

   The sending end of a channel.  An interpreter may use this to
   send objects to another interpreter.  Any type supported by
   Interpreter.set_main_attrs() will be supported here, though
   at first only a few of the simple, immutable builtin types
   will be supported.

   id -> int:

      The channel's unique ID.  The "recv" end has the same one.

   send(obj, *, timeout=None):

      Send the object (i.e. its data) to the "recv" end of the
      channel.  Wait until the object is received.  If the object
      is not shareable then ValueError is raised.

      The builtin memoryview is supported, so sending a buffer
      across involves first wrapping the object in a memoryview
      and then sending that.

   send_nowait(obj):

      Send the object to the "recv" end of the channel.  This
      behaves the same as "send()", except for the waiting part.
      If no interpreter is currently receiving (waiting on the
      other end) then queue the object and return False.  Otherwise
      return True.

공유 객체에 대한 주의 사항

다시 말해, Python 객체는 인터프리터 간에 공유되지 않습니다. 그러나 경우에 따라 이러한 객체가 래핑하는 데이터는 단순히 복사되는 것이 아니라 실제로 공유됩니다. 한 가지 예로 PEP 3118 버퍼를 들 수 있습니다.

이러한 경우 원래 인터프리터의 객체는 다른 인터프리터의 공유 데이터가 더 이상 사용되지 않을 때까지 살아 있는 상태로 유지됩니다. 그러면 이전에 공유된 데이터와 함께 원래 인터프리터에서 정상적으로 객체 소멸이 일어날 수 있습니다.

문서

interpreters 모듈에 대한 새로운 표준 라이브러리 문서 페이지에는 다음 내용이 포함됩니다.

  • (맨 위에) 여러 인터프리터에 대한 지원이 확장 모듈에 요구되지 않는다는 명확한 설명
  • 서브인터프리터에 대한 설명
  • 여러 인터프리터를 사용하고 서로 통신하는 방법에 대한 간단한 예제
  • 여러 인터프리터 사용의 제한 사항 요약
  • (확장 모듈 유지 관리자용) 여러 인터프리터 호환성을 보장하기 위한 리소스 링크
  • 이 PEP의 API 정보 대부분

확장 모듈 유지 관리자를 위한 리소스 문서는 이미 Isolating Extension Modules 하우투 페이지에 있습니다. 추가적인 도움말은 그곳에 추가됩니다. 예를 들어 자체적으로 서브인터프리터와 호환되지 않는 전역 상태를 유지하는 연결된 라이브러리를 처리하는 전략을 논의하는 것이 도움이 될 수 있습니다.

문서가 새로운 interpreters 모듈이 확장 모듈 유지 관리자에게 미칠 수 있는 부정적 영향을 완화하는 데 큰 역할을 한다는 점에 유의하십시오.

또한 호환되지 않는 확장 모듈에 대한 ImportError는 여러 인터프리터 호환성이 없기 때문에 발생하며 확장 모듈이 이를 제공할 필요가 없다는 점을 명확히 설명하도록 업데이트됩니다. 이를 통해 사용자의 기대를 적절히 설정할 수 있습니다.

대안

새 모듈을 추가하는 대신 concurrent.futures에 인터프리터 지원을 추가하는 것도 한 가지 가능한 대안입니다. 그렇게 해서는 안 되는 이유는 여러 가지가 있습니다.

  • 여러 인터프리터 지원을 찾을 때 가장 분명한 위치는 “threading” 등과 마찬가지로 “interpreters” 모듈입니다.
  • concurrent.futures는 함수를 실행하는 데 중점을 두지만, 현재 한 인터프리터의 함수를 다른 인터프리터에서 실행할 좋은 방법은 없습니다.

multiprocessing 모듈에서의 지원에도 유사한 논리가 적용됩니다.

미결 사항

  • interp.exec()가 현재 스레드에서 실행된다는 점이 너무 혼란스럽지는 않겠습니까?
  • 지금 interp.exec()Interpreter.set_main_attrs()Interpreter.get_main_attr()에 피클링 대체 수단을 추가해야 합니까?
  • 지금 interp.exec()에서 (제한적인) 함수를 지원해야 합니까?
  • Interpreter.close()Interpreter.destroy()로 이름을 변경합니까?
  • 채널이 있으므로 Interpreter.get_main_attr()를 제거합니까?
  • 채널을 자체 PEP로 해야 합니까?

추후 기능

이 제안을 최소한으로 유지하기 위해 다음 기능은 추후 검토 대상으로 남겨 두었습니다. 이는 해당 기능 중 어느 것에 대해서도 반대하는 판단이 아니라, 추후로 미룬다는 의미임을 유의하십시오. 그렇다고 해도 각 항목은 나름대로 타당하다고 볼 수 있습니다.

편의 API 추가

새 모듈의 가설적인 거친 부분을 완화할 수 있을 것으로 생각되는 사항이 몇 가지 있습니다:

  • Interpreter.run() 또는 Interpreter.call()와 같은 것을 추가하여 interp.exec()를 호출하고 pickle로 폴백합니다.
  • Interpreter.set_main_attrs()Interpreter.get_main_attr()에서 pickle로 폴백합니다.

이것이 실제로 문제가 되는 지점임이 입증되면 쉽게 구현할 수 있습니다.

현재 스레드에서 실행되는 인터프리터에 대한 혼동 가능성 방지

흔히 혼동되는 한 가지 사항은 Interpreter.exec()가 현재 OS 스레드에서 실행되어 현재 Python 스레드를 일시적으로 차단한다는 것입니다. 그러한 혼동을 피하기 위한 조치를 취할 가치가 있을 수 있습니다.

이 가설적인 문제에 대한 가능한 해결책은 다음과 같습니다:

  • 기본적으로 새 스레드에서 실행합니까?
  • Interpreter.exec_in_thread()를 추가합니까?
  • Interpreter.exec_in_current_thread()를 추가합니까?

이 PEP의 이전 버전에서는 메서드가 interp.run()였습니다. 문서를 통해 사용자에게 안내하는 것과 함께, interp.exec()로 단순히 변경하는 것만으로도 혼동이 충분히 줄어들 것입니다. 실제로 문제가 되는 것으로 밝혀지면 그 시점에 대안 중 하나를 추진할 수 있습니다.

“실행 중”과 “스레드를 보유함”의 구분 명확히 하기

Interpreter.is_running()Interpreter.exec() (또는 유사한 메서드)가 어딘가에서 실행 중인지 여부를 구체적으로 나타냅니다. 인터프리터에 실행 중인 하위 스레드가 있는지에 대해서는 아무것도 말하지 않습니다. 이 정보가 유용할 수 있습니다.

할 수 있는 몇 가지 사항은 다음과 같습니다:

  • Interpreter.is_running()의 이름을 Interpreter.is_running_main()으로 변경합니다.
  • Interpreter.has_threads()를 추가하여 Interpreter.is_running()을 보완합니다.
  • Interpreter.is_running(main=True, threads=False)로 확장합니다.

이 중 어느 것도 긴급하지 않으며, 원한다면 나중에 무엇이든 수행할 수 있습니다.

공유를 위한 Dunder 메서드

tp_xid에 대응하는 __xid__와 같은 특수 메서드를 추가할 수 있습니다. 최소한 Python 타입이 tp_xid를 구현하는 다른 타입으로 자신의 인스턴스를 변환할 수 있게 됩니다.

문제는 이 기능을 Python 코드에 노출하면 아직 탐구되지 않은 복잡성이 상당히 발생하며, 그 복잡성을 조사해야 할 설득력 있는 이유도 없다는 점입니다.

인터프리터.call() 호출

서브인터프리터에서 기존 함수를 직접 실행할 수 있다면 편리할 것입니다. Interpreter.exec()를 조정하여 이를 지원하거나 call() 메서드를 추가할 수 있습니다.:

Interpreter.call(f, *args, **kwargs)

이는 큐를 통해 인터프리터 간에 객체를 공유할 때와 동일한 문제를 겪습니다. 최소한의 해결책(소스 문자열 실행)만으로도 기능을 제공하여 탐구할 수 있는 상태로 만들기에 충분합니다.

인터프리터.run_in_thread()에서 실행

이 메서드는 스레드에서 interp.exec()를 호출해 줍니다. threading.Threadinterp.exec()만 사용하여 이를 구현하는 것은 비교적 간단하므로 제외했습니다.

동기화 프리미티브

threading 모듈은 동시 작업을 조정하기 위한 여러 동기화 프리미티브를 제공합니다. 이는 스레딩의 공유 상태 특성 때문에 특히 필요합니다. 반면 인터프리터는 상태를 공유하지 않습니다. 데이터 공유는 런타임의 공유 가능한 객체 기능으로 제한되므로 명시적 동기화가 필요하지 않습니다. 향후 CPython 인터프리터에 어떤 형태로든 선택적 공유 상태 지원이 추가된다면, 동일한 작업을 통해 해당 요구를 충족하는 동기화 프리미티브를 도입할 수 있습니다.

CSP 라이브러리

csp 모듈은 이 PEP가 제공하는 기능에서 크게 벗어나지 않습니다. 그러나 이러한 모듈을 추가하는 것은 이 제안의 최소주의적 목표를 벗어납니다.

구문 지원

Go 언어는 CSP에 기반한 동시성 모델을 제공하므로, 여러 인터프리터가 지원하는 동시성 모델과 유사합니다. 그러나 Go 는 동시성을 일급 기능으로 만들기 위해 구문 지원뿐 아니라 여러 내장 동시성 프리미티브도 제공합니다. 인터프리터를 사용하여 Python에 이와 유사한 구문 및 내장 지원을 추가할 수도 있습니다. 그러나 이는 이 PEP의 범위를 훨씬 벗어납니다!

멀티프로세싱

multiprocessing 모듈은 스레드와 프로세스를 지원하는 것과 같은 방식으로 인터프리터를 지원할 수 있습니다. 실제로 이 모듈의 유지 관리자인 Davin Potts는 이것이 합리적인 기능 요청이라고 밝혔습니다. 그러나 이는 이 PEP의 좁은 범위를 벗어납니다.

C 확장 기능의 선택적 사용/사용 해제

관련 PEP 489에서 도입된 PyModuleDef_Slot을 사용하면 C 확장 모듈이 다중 인터프리터 지원을 사용하지 않도록 선택할 수 있는 메커니즘을 손쉽게 추가할 수 있습니다. 그러면 가져오기 메커니즘은 서브인터프리터에서 작동할 때 모듈의 지원 여부를 확인해야 합니다. 지원되지 않으면 ImportError를 발생시킵니다.

또는 여러 인터프리터 지원을 선택적으로 활성화하는 방식을 지원할 수 있습니다. 그러나 그렇게 하면 옵트아웃 방식보다 (불필요하게) 훨씬 더 많은 모듈이 제외될 가능성이 높습니다. 또한 PEP 489에서는 확장 모듈이 해당 PEP의 메커니즘을 사용하면 여러 인터프리터 지원을 의미한다고 정의했다는 점에 유의하십시오.

ModuleDef 슬롯을 추가하고 임포트 메커니즘을 수정하는 작업의 범위는 간단하지 않지만, 그만한 가치가 있을 수 있습니다. 이는 서브인터프리터에서 중단되는 확장 모듈이 얼마나 많은지에 전적으로 달려 있습니다. mod_wsgi를 통해 알고 있는 사례가 비교적 적으므로, 이 작업은 나중으로 미뤄도 됩니다.

채널 오염

CSP에는 채널을 오염시키는 개념이 있습니다. 채널이 오염되면 해당 채널에 대한 send() 또는 recv() 호출은 특수 예외를 발생시키며, 오염된 채널을 사용하려 한 인터프리터에서 실행을 사실상 종료합니다.

이는 채널 양쪽 끝에 poison() 메서드를 추가하여 구현할 수 있습니다. close() 메서드를 (대부분) 이러한 방식으로 사용할 수 있지만, 이러한 의미론은 비교적 특수하므로 기다려도 됩니다.

__main__ 재설정

제안된 방식에서는 Interpreter.exec()를 호출할 때마다 인터프리터의 기존 __main__모듈 네임스페이스에서 실행합니다. 즉, interp.exec() 호출 사이에 데이터가 해당 네임스페이스에 유지됩니다. 때로는 이것이 바람직하지 않아 새 __main__에서 실행하려 할 수 있습니다. 또한 더 이상 사용하지 않는 객체를 그곳에 남기고 싶지 않을 수도 있습니다.

다음 방법은 너무 많이 지우기 때문에 제대로 작동하지 않는다는 점에 유의하십시오(예를 들어 __name__ 및 다른 “__dunder__” 속성까지 지웁니다).:

interp.exec('globals().clear()')

가능한 해결책은 다음과 같습니다.

  • interp.exec() 호출 후 __main__을 재설정할지 나타내는 create() 인자
  • 나중에 선택적으로 활성화하거나 비활성화할 수 있도록 하는 Interpreter.reset_main 플래그
  • 원할 때 선택적으로 활성화할 수 있는 Interpreter.reset_main() 메서드
  • importlib.util.reset_globals() [reset_globals]

또한 __main__을 재설정해도 다른 모듈에 저장된 상태에는 아무런 영향을 주지 않는다는 점에 유의하십시오. 따라서 어떤 해결책이든 무엇을 재설정하는지 그 범위를 명확히 해야 합니다. 모듈을 로드하기 전에 모듈을 지우지 않는 reload()와 달리, 어떤 모듈이든(또는 모든 모듈을) 재설정할 수 있는 메커니즘을 고안하는 것도 가능할 것입니다.

어쨌든 __main__은 인터프리터의 실행 네임스페이스이므로, 이를 재설정하는 것은 다른 모듈을 재설정하는 것보다 인터프리터 및 인터프리터의 동적 상태와 훨씬 더 직접적으로 연관됩니다. 따라서 더 일반적인 모듈 재설정 메커니즘은 불필요한 것으로 판명될 수 있습니다.

이는 초기에는 중요한 기능이 아닙니다. 필요하다면 나중으로 미뤄도 됩니다.

인터프리터 상태 재설정

기존 서브인터프리터를 새로 실행하는 대신 재사용하면 좋을 수 있습니다. 인터프리터에는 __main__ 모듈만 있는 것보다 훨씬 더 많은 상태가 있으므로, 인터프리터를 깨끗한 새 상태로 되돌리기가 그리 쉽지 않습니다. 실제로 Python 코드에서 재설정할 수 없는 상태의 일부가 있을 수도 있습니다.

가능한 해결책은 Interpreter.reset() 메서드를 추가하는 것입니다. 그러면 인터프리터가 새로 생성되었을 때의 상태로 되돌아갑니다. 실행 중인 인터프리터에서 호출하면 실패하므로 (따라서 주 인터프리터는 결코 재설정할 수 없습니다). 이는 새 인터프리터를 생성하는 것보다 더 효율적일 가능성이 높지만, 이는 이후 인터프리터 생성에 어떤 최적화가 적용될지에 달려 있습니다.

이는 Python 코드에서 달리 사용할 수 없는 기능을 잠재적으로 제공하지만, 근본적인 기능은 아닙니다. 따라서 여기서는 미니멀리즘의 취지에 따라 이 작업은 미룰 수 있습니다. 어쨌든 PEP 이후에 이를 추가하는 것이 논란이 되지는 않을 것 같습니다.

기존 인터프리터의 상태 복사

이와 관련하여 기존 인터프리터를 기반으로 새 인터프리터를 생성하는 것을 지원하면 유용할 수 있습니다. 예를 들어 Interpreter.copy()와 같이 할 수 있습니다. 이는 인터프리터 메모리의 스냅샷을 만들 수 있다는 아이디어와 연결되며, 일반적으로 CPython 시작 또는 새 인터프리터 생성 속도를 높일 수 있습니다. 앞서 설명한 가상의 Interpreter.reset()에도 동일한 메커니즘을 사용할 수 있습니다.

공유 가능한 파일 디스크립터 및 소켓

파일 디스크립터와 소켓은 프로세스 전역 리소스이므로, 이를 공유 가능하게 만드는 것은 합리적인 생각입니다. 지원되는 공유 가능 타입을 확장하기 위한 첫 번째 시도의 좋은 후보가 될 것입니다. 초기 API에 반드시 필요한 것은 아닙니다.

async와의 통합

Antoine Pitrou에 따르면 [async]:

Has any thought been given to how FIFOs could integrate with async
code driven by an event loop (e.g. asyncio)?  I think the model of
executing several asyncio (or Tornado) applications each in their
own subinterpreter may prove quite interesting to reconcile multi-
core concurrency with ease of programming.  That would require the
FIFOs to be able to synchronize on something an event loop can wait
on (probably a file descriptor?).

다중 인터프리터 지원의 기본 기능은 async에 의존하지 않으며 나중에 추가할 수 있습니다.

가능한 해결책은 블로킹 채널 메서드(recv(), 및 send())의 async 구현을 제공하는 것입니다.

또는 “준비 완료 콜백”을 사용하여 async 시나리오에서의 사용을 단순화할 수 있습니다. 이는 recv_nowait()send_nowait() 채널 메서드에 선택적 callback (키워드 전용) 매개변수를 추가하는 것을 의미합니다. 객체가 전송되거나 수신되면 (각각) 콜백이 호출됩니다.

(채널을 버퍼링하면 준비 완료 콜백의 중요성이 줄어든다는 점에 유의하십시오.)

이터레이션 지원

RecvChannel에서 (__iter__() 또는 _next__()를 통해) 이터레이션을 지원하면 유용할 수 있습니다. 간단한 구현에서는 파일이 이터레이션하는 방식과 유사하게 recv() 메서드를 사용합니다. 이는 근본적인 기능이 아니고 간단한 유사 기능이 있으므로, 이터레이션 지원 추가는 나중으로 미룰 수 있습니다.

채널 컨텍스트 관리자

RecvChannelSendChannel에 대한 컨텍스트 관리자 지원이 유용할 수 있습니다. close() 호출(또는 어쩌면 release() 호출)을 파일에서 하는 것처럼 감싸면 구현은 간단할 것입니다. 반복과 마찬가지로 이 작업은 기다려도 됩니다.

파이프 및 큐

제안된 “os.pipe()” 객체 전달 메커니즘을 사용하면, 여러 인터프리터의 최소한으로 유용한 기능을 달성하는 데 다른 유사한 기본 유형은 엄밀히 필요하지 않습니다. 이러한 유형에는 파이프(버퍼링되지 않은 채널과 같지만 일대일인 유형)와 큐(채널과 같지만 더 일반적인 유형)가 포함됩니다. 자세한 내용은 아래의 Rejected Ideas에서 확인하십시오.

이러한 유형은 이 제안의 일부가 아니지만, 동시성 맥락에서는 여전히 유용할 수 있습니다. 나중에 추가하는 것은 전적으로 합리적입니다. 이들은 채널을 둘러싼 래퍼로 간단히 구현할 수 있습니다. 또는 효율성을 위해 채널과 동일한 낮은 수준에서 구현할 수도 있습니다.

send()에서 잠금 반환

채널을 통해 객체를 보낼 때, 해당 객체가 반대편에서 수신되는 시점을 알 방법이 없습니다. 이를 우회하는 한 가지 방법은 SendChannel.send()에서 잠긴 threading.Lock을 반환하고, 객체가 수신되면 잠금을 해제하는 것입니다.

또는 제안된 SendChannel.send()(블로킹)와 SendChannel.send_nowait()은 사용자를 혼란스럽게 할 가능성이 낮은 명시적 구분을 제공합니다.

잠금 반환은 버퍼링된 채널(즉, 큐)에서 중요하다는 점에 유의하십시오. 버퍼링되지 않은 채널에서는 문제가 되지 않습니다.

채널에서 우선순위 지정 지원

간단한 예는 표준 라이브러리의 queue.PriorityQueue입니다.

설정 상속 지원(및 그 밖의 기능?)

새 인터프리터를 만들 때 일부 항목을 새 인터프리터에 “상속”하고 싶다는 것을 나타낼 수 있으면 유용하다고 생각하는 사용자들이 있을 수 있습니다. 이 메커니즘은 엄격한 복사 방식일 수도 있고, 쓰기 시 복사 방식일 수도 있습니다. 동기를 부여하는 예는 warnings 모듈입니다(예: 필터 복사).

이 기능은 중요하지도 않고 널리 유용하지도 않으므로, 관심이 생길 때까지 기다려도 됩니다. 특히 복잡한 객체를 처리할 때, 그리고 무엇보다 변경 가능한 복잡한 객체를 담은 변경 가능한 컨테이너를 처리할 때 두 제안된 해결책 모두 상당한 작업이 필요합니다.

예외를 공유 가능하게 만들기

run()호출에서 예외가 전파되므로, 예외를 공유 가능하게 만드는 것은 큰 도약이 아닙니다. 그러나 다른 곳에서 언급했듯이, 이는 필수적이지도 않고 특히 일반적이지도 않으므로 해당 작업은 나중으로 미뤄도 됩니다.

직렬화를 통해 모든 것을 공유 가능하게 만들기

pickle(또는 marshal)을 사용하여 모든 것을 직렬화하고 공유 가능하게 만들 수 있습니다. 이렇게 하면 잠재적으로 비효율적일 수 있지만, 결국에는 편의성의 문제일 수 있습니다. 나중에 추가할 수 있지만, 나중에 제거하려고 하면 훨씬 더 큰 어려움이 따를 것입니다.

RunFailedError.__cause__을 지연 평가하도록 하십시오

서브 인터프리터에서(interp.exec()에서 발생한) 처리되지 않은 예외는 호출 인터프리터로 복사되고, 이후 발생하는 RunFailedError__cause__로 설정됩니다. 이 복사 과정에는 호출 인터프리터에서 일종의 역직렬화가 포함되며, 이는 비용이 많이 들 수 있지만(예: 임포트로 인해) 항상 필요한 것은 아닙니다.

따라서 직렬화된 예외를 감싸고 필요할 때만 역직렬화하는 ExceptionProxy형식을 사용하는 것이 유용할 수 있습니다. 이는 ExceptionProxy__getattribute__()를 통해 수행하거나, 어쩌면 RunFailedError.resolve()를 통해 수행할 수 있습니다(이 메서드는 역직렬화된 예외를 발생시키고 RunFailedError.__cause__를 해당 예외로 설정합니다.

또한 RunFailedError.__cause__를 지연 역직렬화를 수행하는 디스크립터로 만들고 RunFailedError 인스턴스에서 __cause__를 설정하는 것도 타당할 수 있습니다.

interp.exec()에서 값을 반환하기

현재 interp.exec()는 항상 None을 반환합니다. 한 가지 아이디어는 서브 인터프리터가 실행한 항목의 반환값을 반환하는 것입니다. 그러나 현재로서는 타당하지 않습니다. 사람들이 실행할 수 있는 유일한 것은 코드 문자열(즉, 스크립트)입니다. 이는 PyRun_StringFlags(), exec(), 또는 모듈 본문과 동일합니다. 이들 중 어느 것도 아무것도 “반환”하지 않습니다. interp.exec()가 함수 등을 지원하게 되면 이를 다시 검토할 수 있습니다.

공유 가능한 동기화 프리미티브 추가

이는 인터프리터들이 기본 뮤텍스를 실제로 공유하는 _threading.Lock(또는 이와 유사한 것)이 됩니다. 가장 큰 우려는 잠금과 격리된 인터프리터가 잘 맞지 않을 수 있다는 점입니다(Go에서 배운 내용입니다).

큰 어려움 없이 바람직한 것으로 입증되면 나중에 이를 추가할 수 있습니다.

SystemExit와 KeyboardInterrupt를 다르게 전파하기

BaseException을 상속하는 예외 형식 중 Exception을 제외한 형식은 일반적으로 특별히 처리됩니다. 이러한 형식은 다음과 같습니다: KeyboardInterrupt, SystemExit, GeneratorExit. interp.exec()에서 전파할 때 이들을 특별히 처리하는 것이 타당할 수 있습니다. 다음과 같은 선택지가 있습니다.:

* propagate like normal via RunFailedError
* do not propagate (handle them somehow in the subinterpreter)
* propagate them directly (avoid RunFailedError)
* propagate them directly (set RunFailedError as __cause__)

이들을 다르게 처리하는 것에 대해서는 걱정하지 않을 예정입니다. 스레드는 이미 SystemExit를 무시하므로, 현재로서는 해당 방식을 따르겠습니다.

채널 끝 클래스에 명시적인 release() 및 close() 추가

이후 전역 사용을 차단하도록 채널을 명시적으로 닫을 방법이 있으면 편리할 수 있습니다. 마찬가지로 현재 인터프리터에 대해 채널 끝 중 하나를 명시적으로 해제할 방법도 유용할 수 있습니다. 무엇보다도 이러한 메커니즘은 객체를 채널을 통해 직접 전달할 때 필요한 추가적인 보일러플레이트 없이 인터프리터 간에 전체 상태를 전달하는 데 유용합니다.

이해하기 어렵게 만들지 않으면서 자동 release()/close()를 올바르게 처리하는 것이 과제입니다. 이는 비어 있지 않은 채널을 다룰 때 특히 그렇습니다. 현재로서는 release()/close() 없이도 충분히 처리할 수 있어야 합니다.

SendChannel.send_buffer() 추가

이 메서드를 사용하면 객체가 PEP 3118 버퍼 프로토콜을 지원하는 경우(예: memoryview) 복사 없이 채널을 통해 전송할 수 있습니다.

이는 채널의 기본 기능은 아니므로 큰 문제 없이 나중에 추가할 수 있습니다.

스레드에서 자동 실행

이 PEP는 서브인터프리터와 스레드를 엄격히 분리할 것을 제안합니다. 스레드에서 실행하려면 직접 스레드를 생성한 후 그 안에서 interp.exec()를 호출해야 합니다. 그러나 interp.exec()가 이 작업을 대신 수행할 수 있다면 상용구 코드가 줄어들어 편리할 수 있습니다.

또한 사용자는 그렇지 않은 경우보다 훨씬 더 자주 스레드에서 실행하기를 원할 것으로 예상합니다. 따라서 이를 기본 동작으로 만드는 것이 타당합니다. 현재 스레드에서 실행하는 작업을 가능하게 하도록 interp.exec()에 키워드 전용 매개변수 “threaded”(기본값 True)를 추가합니다.

거부된 아이디어

명시적 채널 연결

인터프리터는 recv()send() 호출 시 채널과 암묵적으로 연결됩니다. release() 호출 시 연결이 해제됩니다. 대안은 명시적 메서드를 사용하는 것입니다. 이는 Interpreter 객체의 add_channel()remove_channel() 메서드이거나, 채널 객체에 이와 유사한 메서드를 제공하는 방식일 수 있습니다.

실제로는 이러한 수준의 관리가 사용자에게 필요하지 않습니다. 따라서 더 명시적인 지원을 추가하면 API만 복잡해집니다.

파이프 기반 API 추가

파이프는 정확히 두 인터프리터 사이의 단방향 FIFO입니다. 대부분의 사용 사례에서는 이것으로 충분합니다. 또한 구현을 잠재적으로 단순화할 수도 있습니다. 그러나 채널을 통해 다대다 단방향 FIFO를 지원하는 것은 그리 큰 작업이 아닙니다. 또한 파이프를 사용하면 파이프에 이름을 지정해야 하므로 API가 약간 더 복잡해집니다.

큐 기반 API 추가

큐와 버퍼링된 채널은 거의 같은 것입니다. 가장 큰 차이점은 채널이 컨텍스트(즉, 연결된 인터프리터)와 더 강하게 연관된다는 점입니다.

표준 라이브러리의 queue.Queue와 혼동하지 않도록 “Queue” 대신 “Channel”이라는 이름을 사용했습니다.

“enumerate”

list_all()함수는 모든 인터프리터의 목록을 제공합니다. 제안된 API에 부분적으로 영감을 준 threading 모듈에서는 이 함수를 enumerate()라고 부릅니다. 여기서는 threading API에 이미 익숙하지 않은 Python 사용자를 혼동시키지 않도록 이름을 다르게 지정했습니다. 그러한 사용자에게 “enumerate”는 다소 불명확하지만 “list_all”은 명확합니다.

인터프리터 간 예외 누출을 방지하기 위한 대안

함수 호출에서는 처리되지 않은 예외가 호출 프레임으로 전파됩니다. interp.exec()에도 동일한 접근 방식을 사용할 수 있습니다. 그러나 이는 예외 객체가 인터프리터 간 경계를 넘어 누출된다는 의미입니다. 마찬가지로 트레이스백의 프레임도 잠재적으로 누출됩니다.

현재는 문제가 아닐 수 있지만, 메모리 관리 측면에서 인터프리터의 격리가 향상되면 문제가 됩니다(인터프리터 간 GIL 공유를 중단하려면 이러한 격리가 필요합니다). 대신 RunFailedError를 발생시키는 방식으로 예외 전파 방식의 의미를 확정했으며, 이 예외의 __cause__가 원래 예외와 트레이스백에 대한 안전한 프록시를 감쌉니다.

거부된 가능한 해결책:

  • 원래 인터프리터에서 예외와 트레이스백을 재현하여 이를 발생시킵니다.
  • 원래 예외와 트레이스백을 프록시하는 RunFailedError의 서브클래스를 발생시킵니다.
  • RunFailedError 대신 RuntimeError를 발생시킵니다.
  • 경계에서 변환합니다(subprocess.CalledProcessError처럼)(인터프리터 간 표현이 필요합니다).
  • Interpreter.excepthook을 통한 사용자 지정을 지원합니다(인터프리터 간 표현이 필요합니다).
  • 경계에서 프록시로 감쌉니다(err.raise()와 같은 기능을 지원하여 트레이스백을 전파하는 것도 포함합니다).
  • 예외를 발생시키는 대신 interp.exec()에서 예외 또는 해당 프록시를 반환합니다.
  • 결과 객체를 반환합니다(subprocess가 하는 것처럼) [result-object] (불필요한 복잡성입니까?)
  • 예외를 버리고, 사용자가 interp.exec()에 전달하는 스크립트에서 처리되지 않은 예외를 명시적으로 처리하도록 합니다(채널을 통해 오류 정보를 외부로 전달할 수 있습니다). 스레드를 사용할 때도 이와 비슷한 작업을 해야 합니다.

새 인터프리터마다 항상 자체 스레드를 연결합니다.

C-API에서 구현된 방식에 따르면 인터프리터는 본질적으로 어떤 스레드에도 연결되지 않습니다. 또한 Python이 생성했는지 여부와 관계없이 기존의 모든 스레드에서 실행됩니다. 먼저 해당 스레드에서 인터프리터의 스레드 상태(PyThreadState) 중 하나를 활성화하기만 하면 됩니다. 이는 동일한 스레드에서 둘 이상의 인터프리터를 실행할 수 있음을 의미합니다(물론 동시에 실행할 수는 없습니다).

제안된 모듈은 이 동작을 유지합니다. 인터프리터는 스레드에 종속되지 않습니다. Interpreter.exec()호출만 스레드에 종속됩니다. 그러나 이 PEP의 핵심 목표 중 하나는 인간 중심의 동시성 모델을 제공하는 것입니다. 이러한 점을 고려하면, 개념적 관점에서 각 인터프리터가 자체 스레드와 연결되어 있다면 모듈을 더 쉽게 이해할 수 있을 수도 있습니다.

이는 interpreters.create()가 새 스레드를 생성하고 Interpreter.exec()가 해당 스레드에서만 실행되며(그 밖의 작업은 전혀 실행되지 않으며), 이를 의미합니다. 그 이점은 사용자가 Interpreter.exec()호출을 새 threading.Thread에 감쌀 필요가 없다는 것입니다. 또한 인터프리터가 실행되는 동안 현재 인터프리터(현재 스레드에 있는 인터프리터)를 실수로 일시 중지할 상황에 놓이지도 않습니다.

이 아이디어는 이점이 작고 비용이 크기 때문에 거부됩니다. C-API의 기능과 차이가 잠재적으로 혼란을 일으킬 수 있습니다. 스레드의 암시적 생성은 마법처럼 이루어집니다. 스레드를 일찍 생성하면 잠재적으로 낭비가 발생합니다. 기존 스레드에서 임의의 인터프리터를 실행할 수 없으면 일부 유효한 사용 사례가 불가능해져 사용자가 좌절할 수 있습니다. 인터프리터를 스레드에 연결하려면 런타임을 추가로 수정해야 합니다. 또한 모듈의 구현이 지나치게 복잡해집니다. 마지막으로, 모듈을 더 쉽게 이해할 수 있게 해 주지도 않을 수 있습니다.

사용할 때만 인터프리터 연결

recv(), send() 등을 호출할 때만 인터프리터를 채널 끝점에 연결합니다.

이 작업은 잠재적으로 혼란을 일으키며, 원래 (생성한) 인터프리터에서 사용되기 전에 채널이 자동으로 닫히는 예상치 못한 경쟁 상태를 초래할 수도 있습니다.

인터프리터.exec()에 대한 동시 호출 여러 건을 허용합니다.

특히 Interpreter.exec()가 새 스레드를 대신 관리한다면 이는 타당할 것입니다(하지만 이 방안은 거부되었습니다). 본질적으로 각 호출은 독립적으로 실행되며, 각 인터프리터가 여러 스레드를 가질 수 있으므로 좁은 기술적 관점에서는 대부분 문제가 없을 것입니다.

문제는 인터프리터에 __main__ 모듈이 하나뿐이므로 동시에 실행되는 Interpreter.exec() 호출이 __main__ 공유를 조정해야 하거나 새로운 메커니즘을 고안해야 한다는 점입니다. 어느 쪽도 실행할 가치가 있을 만큼 단순하지 않습니다.

RunFailedError에 “reraise” 메서드 추가

__cause__RunFailedError에 설정하면 더 유용한 트레이스백을 생성하는 데 도움이 되지만, 원래 오류를 처리할 때는 덜 유용합니다. 이를 돕기 위해 RunFailedError.reraise()를 추가할 수 있습니다. 이 메서드를 사용하면 다음 패턴을 가능하게 할 수 있습니다.:

try:
    try:
        interp.exec(script)
    except RunFailedError as exc:
        exc.reraise()
except MyException:
    ...

__reraise__ 프로토콜이 존재한다면 이는 더욱 간단해질 것입니다.

그렇다고 해도 이는 전혀 필요하지 않습니다. __cause__를 사용하는 것으로 충분합니다.:

try:
    try:
        interp.exec(script)
    except RunFailedError as exc:
        raise exc.__cause__
except MyException:
    ...

극단적인 경우에는 약간의 추가 보일러플레이트가 필요할 수 있다는 점에 유의하십시오.:

try:
    try:
        interp.exec(script)
    except RunFailedError as exc:
        if exc.__cause__ is not None:
            raise exc.__cause__
        raise  # re-raise
except MyException:
    ...

구현

이 PEP의 구현은 4개 부분으로 구성됩니다:

  • 이 PEP에서 설명하는 고수준 모듈(대부분 저수준 C 확장을 둘러싼 가벼운 래퍼
  • 저수준 C 확장 모듈
  • 저수준 모듈에 필요한 내부 C-API 추가 사항
  • 저수준 모듈을 지원하는 CPython 런타임의 부수적 수정/변경 사항(다른 이점 중에서도)

이는 다양한 완료 수준에 있으며, 아래로 내려갈수록 더 많이 완료되어 있습니다:

  • 고수준 모듈은 기껏해야 대략적으로만 구현되어 있습니다. 하지만 이를 완전히 구현하는 것은 거의 사소한 작업이 될 것입니다.
  • 저수준 모듈은 대부분 완성되어 있습니다. 구현의 대부분은 2018년 12월에 “_xxsubinterpreters” 모듈로 마스터에 병합되었습니다(다중 인터프리터 기능 테스트를 위한 것입니다). 예외 전파 구현만 남아 있으며, 이는 많은 작업을 필요로 하지 않을 것입니다.
  • 필요한 모든 C-API 작업이 완료되었습니다
  • 런타임에서 예상되었던 모든 작업이 완료되었습니다

CPython의 멀티코어 지원을 개선하기 위한 더 큰 프로젝트의 일환으로, PEP 554의 구현 작업이 추적되고 있습니다. [multi-core-project]

참고 자료