Python — язык, а CPython — его основная реализация. CPython разбирает source, создаёт code objects с bytecode и выполняет их во frames. Сам интерпретатор не является Event Loop: асинхронный планировщик появляется только когда программа запускает asyncio.
Разбираем: CPython runtime, память, GIL и asyncio
Этот раздел можно читать до запуска опыта. После теории вернитесь к live trace и сопоставьте каждый шаг с реальным событием.
Модуль, function body и class body являются code blocks и исполняются во frames. В CPython source компилируется в bytecode для evaluation loop; opcodes меняются между версиями. Объекты прежде всего освобождаются reference counting, а cyclic GC дополняет его для циклов. В обычной GIL-сборке один thread одновременно выполняет Python bytecode, хотя blocking I/O освобождает GIL. asyncio поверх этого предоставляет event loop, Tasks, Futures и awaitables.
Интерпретатор, GIL и asyncio — три разных слоя
Сначала определите: выполняется ли bytecode, управляется ли доступ threads к objects или coroutine добровольно отдала управление event loop.
- 01 · CompileSource становится code object
Parser строит синтаксическую структуру, compiler определяет scopes и выпускает bytecode.
- 02 · ExecuteFrame выполняет bytecode
Frame хранит context code block и locals. Evaluation loop исполняет инструкции CPython.
- 03 · CoordinateВыбирается concurrency model
Threads делят память, processes изолируют heaps, asyncio переключает Tasks в точках suspension.
Объект часто освобождается сразу после исчезновения последней сильной ссылки.
Дополняет refcount и обнаруживает изолированные циклы ссылок.
В обычной сборке сериализует Python bytecode threads одного interpreter.
Thread делит heap; process имеет отдельную память и IPC.
Loop возобновляет Tasks после await; blocking-код управление не отдаёт.
Язык задаёт semantics, CPython — implementation. PyPy и другие runtimes могут иначе устроить VM, JIT, память и timing cleanup.
Составные операции и shared mutable state требуют синхронизации; C-extensions и free-threaded builds дополнительно меняют предпосылки.
create_task(A, B) → main достигает await → A/B стартуют → await приостанавливает их → готовый B возобновляется → AЕсли coroutine вызывает time.sleep или долго считает без await, loop не получает управление и остальные Tasks ждут.
Термины этого эксперимента
Сначала поймите слова — затем порядок выполнения.
CPython
Эталонная и наиболее распространённая реализация Python, написанная преимущественно на C.
Code object
Скомпилированное представление code block: bytecode, constants, names и metadata.
Bytecode
Инструкции VM CPython. Это не стабильный machine-code контракт и они меняются между версиями.
Frame
Контекст code block: code object, locals, globals, instruction state и связь с вызовами.
Reference counting
Основной механизм lifetime объектов CPython: объект можно освободить, когда сильных ссылок нет.
Cyclic GC
Collector, находящий недостижимые группы objects, которые ссылаются друг на друга.
GIL
Global Interpreter Lock обычной CPython-сборки, требуемый thread для Python objects и bytecode.
Coroutine
Объект приостанавливаемого вычисления, полученный вызовом async def. Сам ещё не запланирован.
Task
Обёртка asyncio, которая планирует coroutine на event loop и хранит result или exception.
Awaitable
Объект для await: coroutine, Task, Future или реализация соответствующего протокола.
Future
Контейнер будущего результата, связывающий callback/API с ожидающей Task.
Free-threaded build
Опциональная сборка CPython без GIL, требующая готовности libraries и extensions.
Что происходит по шагам
Каждый шаг соответствует наблюдаемому состоянию runtime.
- 01Source разбирается
Parser проверяет grammar и строит AST; SyntaxError возникает до выполнения code block.
- 02Compiler создаёт code object
Имена/scopes классифицируются, literals становятся constants, операции — bytecode.
- 03def создаёт function object
Function связывает code object с globals, defaults и closure.
- 04Вызов создаёт frame
Arguments связываются с parameters, evaluation loop начинает bytecode.
- 05Bindings удерживают objects
Удаление binding снижает reachability; cycles требует дополнительного GC.
- 06Thread получает interpreter
В обычном CPython нужен GIL; на blocking I/O он обычно освобождается.
- 07asyncio возобновляет Task
await возвращает управление loop, а готовность позже делает Task runnable.
- 08CPU выбирает другой путь
Долгий pure-Python CPU обычно выносят в process pool, native code или проверенную free-threaded архитектуру.
Где результат требует оговорки
Эти детали объясняют, почему похожий код иногда даёт другой trace.
async def не запускает body
Вызов создаёт coroutine object. Нужны await, create_task или scheduler.
await не всегда переключает Task
Если awaitable уже готов, реальной suspension может не произойти.
GIL — свойство configuration CPython
Free-threaded builds появились в 3.13, другие implementations имеют свои модели.
Threads полезны для I/O
Blocking I/O и многие native extensions освобождают GIL и перекрывают ожидание.
Processes не делят обычный heap
Startup, IPC и serialization могут стоить дороже короткой CPU-задачи.
del не удаляет object напрямую
Он убирает binding; object живёт при оставшихся ссылках, allocator может сохранить arena.
Bytecode — диагностика, не API
dis полезен, но opcodes зависят от версии. Сначала нужен profiler.
Один asyncio loop обычно в одном thread
Большинство asyncio objects не thread-safe; из другого thread нужны специальные APIs.
Сначала разберитесь, какие части Node участвуют в выполнении.
Затем уберите служебные детали и рассмотрите только главную идею.
После этого сопоставьте модель с кодом, который создаёт live trace.
Минимальная модель без служебного кода
async def fetch_pair():
first = asyncio.create_task(fetch("/first"))
second = asyncio.create_task(fetch("/second"))
return await asyncio.gather(first, second)
# Blocking legacy I/O нужно убрать из loop thread:
data = await asyncio.to_thread(legacy_read, path)Полный код, который выполняет сценарий
Это не альтернативный пример: ниже показаны функции и файлы, используемые кнопкой запуска.
Первый файл — настоящий CPython-сценарий; второй безопасно запускает child process и переводит его JSON Lines в live trace.
#!/usr/bin/env python3
"""Fixed, input-free CPython scenarios used by Runtime Lab."""
from __future__ import annotations
import asyncio
import dis
import gc
import io
import json
import platform
import sys
import time
import weakref
from dataclasses import dataclass
from typing import Any, Iterator
def emit(lane: str, event_type: str, key: str, **data: Any) -> None:
print(
json.dumps(
{"lane": lane, "type": event_type, "key": key, "data": data},
ensure_ascii=False,
),
flush=True,
)
def version_event() -> None:
emit(
"python",
"runtime",
"python.version",
implementation=platform.python_implementation(),
version=platform.python_version(),
)
def run_syntax() -> None:
version_event()
orders = [
{"id": "A-10", "status": "paid", "price": 120, "qty": 2},
{"id": "A-11", "status": "draft", "price": 80, "qty": 1},
{"id": "A-12", "status": "paid", "price": 50, "qty": 3},
]
emit("objects", "state", "syntax.objects", count=len(orders))
paid = [order for order in orders if order["status"] == "paid"]
total = sum(order["price"] * order["qty"] for order in paid)
emit(
"comprehension",
"result",
"syntax.comprehension",
ids=", ".join(order["id"] for order in paid),
total=total,
)
first, *middle, last = [order["id"] for order in orders]
emit(
"sequence",
"result",
"syntax.unpack",
first=first,
middle=middle,
last=last,
)
labels = [f"{index}:{order['id']}" for index, order in enumerate(orders, start=1)]
emit("loop", "result", "syntax.enumerate", labels=", ".join(labels))
def format_order(order: dict[str, Any], *, currency: str = "RUB") -> str:
return f"{order['id']} · {order['price'] * order['qty']} {currency}"
emit(
"function",
"result",
"syntax.function",
rendered=format_order(orders[0], currency="₽"),
)
emit("result", "result", "syntax.result")
@dataclass(slots=True)
class CartLine:
sku: str
price: int
quantity: int = 1
@property
def subtotal(self) -> int:
return self.price * self.quantity
def append_bad(item: str, bucket: list[str] = []) -> list[str]:
bucket.append(item)
return list(bucket)
def append_safe(item: str, bucket: list[str] | None = None) -> list[str]:
target = [] if bucket is None else bucket
target.append(item)
return target
def even_squares(limit: int) -> Iterator[int]:
for value in range(limit):
if value % 2 == 0:
yield value * value
def classify_event(event: dict[str, Any]) -> str:
match event:
case {"type": "order.paid", "payload": {"id": order_id}}:
return f"paid order {order_id}"
case {"type": event_type}:
return f"other event {event_type}"
case _:
return "invalid event"
def run_semantics() -> None:
version_event()
line = CartLine("book", 450, quantity=2)
emit(
"class",
"result",
"semantics.dataclass",
rendered=repr(line),
subtotal=line.subtotal,
)
original = ["node"]
alias = original
alias.append("python")
emit("objects", "mutation", "semantics.alias", shared=original == alias == ["node", "python"])
bad_first = append_bad("api")
bad_second = append_bad("worker")
emit(
"function",
"warning",
"semantics.mutable-default",
first=bad_first,
second=bad_second,
)
safe_first = append_safe("api")
safe_second = append_safe("worker")
emit(
"function",
"result",
"semantics.safe-default",
first=safe_first,
second=safe_second,
)
emit("generator", "result", "semantics.generator", values=list(even_squares(7)))
emit(
"pattern",
"result",
"semantics.match",
label=classify_event({"type": "order.paid", "payload": {"id": "A-42"}}),
)
try:
int("not-a-number")
except ValueError as error:
emit("exception", "caught", "semantics.exception", message=str(error))
stream = io.StringIO()
with stream:
stream.write("cleanup is deterministic")
text = stream.getvalue()
emit("context", "cleanup", "semantics.context", closed=stream.closed, text=text)
emit("result", "result", "semantics.result")
def doubled_total(values: list[int]) -> int:
return sum(value * 2 for value in values)
class CycleNode:
def __init__(self) -> None:
self.peer: CycleNode | None = None
async def traced_task(name: str, delay: float, completion: list[str]) -> str:
emit("asyncio", "start", "asyncio.started", name=name)
await asyncio.sleep(delay)
completion.append(name)
emit("asyncio", "resume", "asyncio.resumed", name=name)
return name
async def measure_timer_while(blocking_call: Any) -> float:
started = time.perf_counter()
timer = asyncio.create_task(asyncio.sleep(0.01))
await blocking_call()
await timer
return max(0.0, (time.perf_counter() - started - 0.01) * 1_000)
async def run_asyncio_round() -> None:
completion: list[str] = []
first = asyncio.create_task(traced_task("A", 0.025, completion))
second = asyncio.create_task(traced_task("B", 0.005, completion))
emit("asyncio", "schedule", "asyncio.created")
await asyncio.gather(first, second)
emit("asyncio", "result", "asyncio.result", order=" → ".join(completion))
async def block_loop() -> None:
time.sleep(0.055)
async def offload_sleep() -> None:
await asyncio.to_thread(time.sleep, 0.055)
blocked_delay = await measure_timer_while(block_loop)
emit("asyncio", "blocking", "asyncio.blocking", delay=round(blocked_delay, 1))
offloaded_delay = await measure_timer_while(offload_sleep)
emit("asyncio", "offload", "asyncio.offload", delay=round(offloaded_delay, 1))
def run_runtime() -> None:
implementation = platform.python_implementation()
gil_probe = getattr(sys, "_is_gil_enabled", None)
gil_enabled = gil_probe() if gil_probe else implementation == "CPython"
emit(
"runtime",
"config",
"runtime.config",
implementation=implementation,
version=platform.python_version(),
gil=gil_enabled,
)
operations = [instruction.opname for instruction in dis.get_instructions(doubled_total)]
emit(
"bytecode",
"result",
"runtime.bytecode",
operations=" → ".join(operations[:12]),
)
frame = sys._getframe()
visible_locals = ", ".join(sorted(name for name in frame.f_locals if not name.startswith("_")))
emit(
"frame",
"state",
"runtime.frame",
functionName=frame.f_code.co_name,
locals=visible_locals,
)
left = CycleNode()
right = CycleNode()
left.peer = right
right.peer = left
left_ref = weakref.ref(left)
right_ref = weakref.ref(right)
alive_before = left_ref() is not None and right_ref() is not None
del left, right
collected = gc.collect()
alive_after = left_ref() is not None or right_ref() is not None
emit(
"gc",
"result",
"runtime.gc",
collected=collected,
aliveBefore=alive_before,
aliveAfter=alive_after,
)
asyncio.run(run_asyncio_round())
emit("result", "result", "runtime.result")
SCENARIOS = {
"syntax": run_syntax,
"semantics": run_semantics,
"runtime": run_runtime,
}
def main() -> None:
scenario = sys.argv[1] if len(sys.argv) > 1 else ""
runner = SCENARIOS.get(scenario)
if runner is None:
raise SystemExit(f"unknown scenario: {scenario}")
runner()
if __name__ == "__main__":
main()
Trace инструментирован сценарием: Python вызывает emit(...) и печатает события как упорядоченные JSON Lines, а Node фиксирует timestamp при получении каждой строки. Названия source/lane задаёт код, это не profiler CPython/asyncio. Node bridge читает вывод без shell, ограничивает время и объём и передаёт события HTTP stream.
Практические шаблоны, которые можно подсмотреть
Сравнивайте цель, код и оговорки — не запоминайте синтаксис без модели.
Coroutine и Task
Различить созданное вычисление и работу.
async def load_user(user_id: int):
return await repository.get(user_id)
coroutine = load_user(7)
task = asyncio.create_task(coroutine)
user = await task- Потерянный coroutine ничего полезного не выполнит.
- Task хранит result или exception.
Параллельное ожидание I/O
Не ждать независимые операции последовательно.
user, orders = await asyncio.gather(
load_user(user_id),
load_orders(user_id),
)- Concurrency не означает threads.
- Errors и cancellation входят в контракт.
Offload blocking I/O
Не блокировать event-loop thread.
def read_legacy_file(path: str) -> bytes:
return legacy_client.read(path)
data = await asyncio.to_thread(read_legacy_file, path)- to_thread не делает library intrinsically async.
- Ограничивайте количество jobs.
CPU в process pool
Использовать несколько cores.
pool = ProcessPoolExecutor()
result = await asyncio.get_running_loop().run_in_executor(
pool, calculate_report, payload
)- Function и payload должны сериализоваться.
- Pool должен быть долгоживущим и bounded.
Диагностика bytecode
Увидеть инструкции VM.
import dis
def total(values):
return sum(value * 2 for value in values)
dis.dis(total)- Opcode names зависят от версии.
- Сначала используйте profiler.
Наблюдение GC
Измерять collector, а не гадать по RSS.
import gc
before = gc.get_stats()
collected = gc.collect()
after = gc.get_stats()- collect в request path не чинит leak.
- Ищите удерживающие ссылки.
Как учебная ошибка превращается в инцидент
Реалистичный сервис: исходный код, наблюдаемая проблема, исправление и причина, по которой оно работает.
Один blocking report замораживает весь asyncio server
Async handler строит тяжёлый PDF и вызывает sync provider. Автор считает, что async def автоматически отправляет весь body в фоновый thread.
До следующего реального suspension coroutine выполняется в event-loop thread. time.sleep, sync I/O и pure-Python CPU задерживают timers, sockets и все остальные Tasks этого loop.
async def build_report(payload: dict) -> bytes:
customer = sync_crm.load(payload["customer_id"])
time.sleep(0.2)
return render_pdf(customer, payload)async def меняет протокол вызова, но не природу sync-функций внутри. Ни sync_crm.load, ни time.sleep, ни render_pdf не отдают управление asyncio.
from concurrent.futures import ProcessPoolExecutor
report_pool = ProcessPoolExecutor(max_workers=2)
async def build_report(payload: dict) -> bytes:
customer = await asyncio.to_thread(
sync_crm.load,
payload["customer_id"],
)
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
report_pool,
render_pdf,
customer,
payload,
)Blocking I/O уходит в bounded thread pool, где ожидание не блокирует loop. Pure-Python CPU выполняется в ограниченном process pool и может использовать другие cores без GIL основного interpreter.
Что делают непривычные вызовы из обоих фрагментов кода.
async def- Определяет coroutine function. Вызов создаёт coroutine object, но body выполняется только под управлением await/scheduler.
asyncio.to_thread(...)- Запускает sync callable в thread pool и возвращает coroutine для ожидания результата без блокировки loop thread.
ProcessPoolExecutor- Управляет ограниченным набором child processes с отдельными interpreters и heaps для CPU parallelism.
get_running_loop()- Возвращает event loop текущего async context и выбрасывает RuntimeError, если active loop отсутствует.
run_in_executor(...)- Передаёт sync callable executor-у и связывает его concurrent result с asyncio Future для await.
max_workers=2- Задаёт bounded concurrency: не позволяет одному process бесконтрольно создавать workers на каждый request.
Популярные заблуждения
Миф слева, корректная модель справа.
Python построчно выполняет source без компиляции.
CPython компилирует code blocks в code objects/bytecode и затем выполняет их.
GIL исключает race conditions.
Shared state и составные операции требуют locks/queues.
asyncio делает blocking library неблокирующей.
Sync-вызов блокирует loop; нужен async API или explicit offload.
await запускает другой thread.
Обычно та же Task и thread отдают управление loop до готовности awaitable.
Threads ускоряют pure-Python CPU.
В обычном CPython bytecode не идёт параллельно; используют processes/native/free-threaded build.
gc.collect гарантированно уменьшает RSS.
Objects становятся доступны allocator, но allocator/OS могут сохранить arenas.
Ответьте своими словами
Если ответ получается объяснить без терминов из документации, ментальная модель уже начала складываться.
- Чем Python отличается от CPython?
- Когда async def начнёт выполнять body?
- Почему time.sleep задержит Tasks?
- Зачем reference counting дополняется cyclic GC?
- Почему GIL не отменяет Lock?
- Когда ProcessPool медленнее последовательного кода?