asyncio module and async / await
async def functions can suspend on await, and the host drives long-running
external calls. The sandbox schedules its own tasks; host event loops execute external coroutines.
The asyncio module exposes exactly three functions:
asyncio.run(coro)— runs a coroutine to completion. Returns the value the coroutinereturns, or re-raises an exception from it.asyncio.gather(*awaitables)— runs awaitables concurrently and returns a list of results. Always behaves asreturn_exceptions=False. Any keyword argument is rejected withNotImplementedError: gather() does not yet support keyword arguments, where CPython raisesTypeError: gather() got an unexpected keyword argument 'X'becausereturn_exceptionsis a real kwarg there.asyncio.sleep(delay, result=None)— waits as the session’ssleepsetting says, then producesresult. See below.
Not implemented (raise AttributeError):
create_task, wait, wait_for, shield, to_thread,
new_event_loop, get_event_loop, get_running_loop, Queue, Lock,
Semaphore, Event, Future, Task, TaskGroup, timeout,
timeout_at, Timeout, as_completed, iscoroutine, ensure_future,
the whole asyncio.subprocess / asyncio.streams / asyncio.protocols
surface.
asyncio.timeout() / asyncio.timeout_at() would be unreachable in any
case: they are async context managers, and async with is rejected at parse
time (see language.md).
async deffunctions andawaitwork; coroutines can call each other.- Coroutines are single-shot. Awaiting the same coroutine object twice
raises
RuntimeError. Store the result, not the coroutine, if you need it again. awaiton a non-awaitable raisesTypeError.async forandasync withare rejected at parse time (see language.md). Async iteration and async context-manager protocols do not exist.- Async comprehensions (
[x async for x in ...]) are rejected at parse time. - There is no
__await__protocol. Awaitables are only the things Monty knows internally: coroutines fromasync def, gather futures, and external function call futures returned by host bindings.
CPython’s asyncio.sleep() returns a coroutine that does nothing until it is
awaited. Monty starts waiting at the call; await produces result when the wait finishes.
The session’s sleep setting determines concurrency (see time.md):
'system': all pools answer with futures, so gathered sleeps overlap and sibling tasks can run meanwhile. Standard Rust execution and the CLI wait inline, making gathered sleeps sequential. An immediately awaited sleep may be answered eagerly if no other task can run.'call_host': async handlers inAsyncMontyand JavaScript allow gathered sleeps to overlap.OSAccessis async by default underAsyncMonty. Sync handlers, includingMontycallbacks, wait sequentially; results are unchanged.'zero': the awaitable settles immediately without yielding to sibling tasks, unlike CPython’ssleep(0). Zero-delay system sleeps also settle without a round trip. Under'call_host', a handler returning a pending future allows sibling tasks to run even for zero delay.
Suspending sleeps count against max_suspensions; system sleeps also count against max_total_sleep.
Waiting consumes neither execution-time limit (see time.md).
What follows from waiting at the call:
asyncio.sleep(...)whose result is never awaited has still started the wait, where CPython runs nothing and warns that the coroutine was never awaited.- A bad
delayraises at the call rather than at theawait. The error is the one CPython’sdelay <= 0produces —TypeError: '<=' not supported between instances of 'str' and 'int'— but it surfaces one step earlier. - The value is a future rather than a coroutine, though
type(...).__name__iscoroutineeither way. Itsrepr()is<coroutine external_future(N)>, not CPython’s<coroutine object sleep at 0x...>, and awaiting it a second time replays the same result where CPython raisesRuntimeError: cannot reuse already awaited coroutine.
delay accepts only real numbers, matching CPython’s delay <= 0: an
__index__-able class is rejected here although time.sleep() accepts it.
A negative delay waits zero seconds instead of raising, as CPython
effectively does, and one past ~9223372036.85 seconds is clamped to that
maximum rather than raising the OverflowError time.sleep() raises (see
time.md).
A NaN delay raises CPython’s ValueError: Invalid delay: NaN (not a number),
but at the call rather than at the await.
Concurrency is cooperative and host-driven. gather suspends Monty whenever
every branch is blocked on an external call, hands the pending calls to the
host, and resumes when the host returns results. There is no preemption, no
threads and no exposed event loop.
When one child of a gather raises, the siblings keep running as they do in CPython.
They resume only when a host result arrives or when another task awaits: Monty’s scheduler runs ready tasks only at
those suspension points and has no idle loop that would otherwise turn.
Code that catches the error and then returns without awaiting again leaves them parked where they were:
import asyncio
async def tick(): ... # awaits a host call
async def raises():
raise ValueError('x')
async def sibling():
await asyncio.gather(tick())
print('sibling finished')
async def main():
try:
await asyncio.gather(sibling(), raises())
except ValueError:
pass
asyncio.run(main())
CPython prints sibling finished here, Monty prints nothing.
An external call a sibling already passed to the host is still resolved by the host.
Its result reaches the sibling; a result arriving for a gather that has already failed is discarded.
Which task runs next also differs.
A task resumed by a host result runs to its next suspension before any other ready task gets a turn, where CPython’s
loop takes them in the order they were woken.
Among the tasks that do resume, only the ordering differs.
This applies to all async code, not only to siblings of a failed gather.
CPython’s gather schedules each awaitable as a task straight away, so the children run whether or not the result is
ever awaited.
Monty holds the awaitables and spawns nothing until the await, so a gather(...) whose result is discarded runs no
code at all:
import asyncio
async def boom():
print('boom ran')
raise ValueError('x')
asyncio.gather(boom()) # CPython prints `boom ran`, Monty runs nothing
Awaiting the gather later still runs the children, and a gather that has already failed re-raises its cached exception on every later await.
Neither does Monty report the exception CPython loses track of here.
CPython prints _GatheringFuture exception was never retrieved, a future: line and the traceback to stderr when a
gather holding an unretrieved exception is collected.
In Monty that gather never ran, and no sandboxed code can write to stderr in any case — print() rejects its file
argument.
CPython nests gathers as deeply as memory allows, and the depth costs nothing beyond the futures themselves.
Monty holds a walk frame per level while it commits the nest, so awaiting a deep nest costs memory on top of what the
nest already occupies.
A session with max_memory set can therefore build a nest it cannot await: the await ends the run with
MemoryError: memory limit exceeded, which sandboxed code cannot catch (see resource_limits.md).
The nesting is not charged against the recursion limit in either interpreter, and a session with no memory limit has
no bound to hit.