Trio: async programming for humans and snake people¶
P.S. your API is a user interface – Kenneth Reitz
The Trio project’s goal is to produce a production-quality, permissively licensed, async/await-native I/O library for Python. Like all async libraries, its main purpose is to help you write programs that do multiple things at the same time with parallelized I/O. A web spider that wants to fetch lots of pages in parallel, a web server that needs to juggle lots of downloads and websocket connections at the same time, a process supervisor monitoring multiple subprocesses... that sort of thing. Compared to other libraries, Trio attempts to distinguish itself with an obsessive focus on usability and correctness. Concurrency is complicated; we try to make it easy to get things right.
Trio was built from the ground up to take advantage of the latest Python features, and draws inspiration from many sources, in particular Dave Beazley’s Curio. The resulting design is radically simpler than older competitors like asyncio and Twisted, yet just as capable. Trio is the Python I/O library I always wanted; I find it makes building I/O-oriented programs easier, less error-prone, and just plain more fun. Perhaps you’ll find the same.
This project is young and still somewhat experimental: the overall design is solid and the existing features are fully tested and documented, but you may encounter missing functionality or rough edges. We do encourage you do use it, but you should read and subscribe to issue #1 to get warning and a chance to give feedback about any compatibility-breaking changes.
Vital statistics:
- Supported environments: Linux, MacOS, or Windows running some kind of Python 3.5-or-better (either CPython or PyPy3 is fine). *BSD and illumus likely work too, but are untested.
- Install:
python3 -m pip install -U trio
(or on Windows, maybepy -3 -m pip install -U trio
). No compiler needed. - Tutorial and reference manual: https://trio.readthedocs.io
- Bug tracker and source code: https://github.com/python-trio/trio
- License: MIT or Apache 2, your choice
- Code of conduct: Contributors are requested to follow our code of conduct in all project spaces.
Tutorial¶
Welcome to the Trio tutorial! Trio is a modern Python library for writing asynchronous applications – often, but not exclusively, asynchronous network applications. Here we’ll try to give a gentle introduction to asynchronous programming with Trio.
We assume that you’re familiar with Python in general, but don’t worry
– we don’t assume you know anything about asynchronous programming or
Python’s new async/await
feature.
Also, we assume that your goal is to use Trio to write interesting
programs, so we won’t go into the nitty-gritty details of how
async/await
is implemented inside the Python interpreter. The word
“coroutine” is never mentioned. The fact is, you really don’t need
to know any of that stuff unless you want to implement a library
like Trio, so we leave it out. We’ll include some links in case you’re
the kind of person who’s curious to know how it works under the hood,
but you should still read this section first, because the internal
details will make much more sense once you understand what it’s all
for.
Before you begin¶
- Make sure you’re using Python 3.5 or newer.
python3 -m pip install --upgrade trio
(or on Windows, maybepy -3 -m pip install --upgrade trio
– details)- Can you
import trio
? If so then you’re good to go!
Async functions¶
Python 3.5 added a major new feature: async functions. Using Trio is all about writing async functions, so lets start there.
An async function is defined like a normal function, except you write
async def
instead of def
:
# A regular function
def regular_double(x):
return 2 * x
# An async function
async def async_double(x):
return 2 * x
“Async” is short for “asynchronous”; we’ll sometimes refer to regular
functions like regular_double
as “synchronous functions”, to
distinguish them from async functions.
From a user’s point of view, there are two differences between an async function and a regular function:
To call an async function, you have to use the
await
keyword. So instead of writingregular_double(3)
, you writeawait async_double(3)
.You can’t use the
await
keyword inside the body of a regular function. If you try it, you’ll get a syntax error:def print_double(x): print(await async_double(x)) # <-- SyntaxError here
But inside an async function,
await
is allowed:async def print_double(x): print(await async_double(x)) # <-- OK!
Now, let’s think about the consequences here: if you need await
to
call an async function, and only async functions can use
await
... here’s a little table:
If a function like this | wants to call a function like this | is it gonna happen? |
---|---|---|
sync | sync | ✓ |
sync | async | NOPE |
async | sync | ✓ |
async | async | ✓ |
So in summary: As a user, the entire advantage of async functions over regular functions is that async functions have a superpower: they can call other async functions.
This immediately raises two questions: how, and why? Specifically:
When your Python program starts up, it’s running regular old sync code. So there’s a chicken-and-the-egg problem: once we’re running an async function we can call other async functions, but how do we call that first async function?
And, if the only reason to write an async function is that it can call other async functions, why on earth would we ever use them in the first place? I mean, as superpowers go this seems a bit pointless. Wouldn’t it be simpler to just... not use any async functions at all?
This is where an async library like Trio comes in. It provides two things:
A runner function, which is a special synchronous function that takes and calls an asynchronous function. In Trio, this is
trio.run
:import trio async def async_double(x): return 2 * x trio.run(async_double, 3) # returns 6
So that answers the “how” part.
A bunch of useful async functions – in particular, functions for doing I/O. So that answers the “why”: these functions are async, and they’re useful, so if you want to use them, you have to write async code. If you think keeping track of these
async
andawait
things is annoying, then too bad – you’ve got no choice in the matter! (Well, OK, you could just not use trio. That’s a legitimate option. But it turns out that theasync/await
stuff is actually a good thing, for reasons we’ll discuss a little bit later.)Here’s an example function that uses
trio.sleep()
. (trio.sleep()
is liketime.sleep()
, but with more async.)import trio async def double_sleep(x): await trio.sleep(2 * x) trio.run(double_sleep, 3) # does nothing for 6 seconds then returns
So it turns out our async_double
function is actually a bad
example. I mean, it works, it’s fine, there’s nothing wrong with it,
but it’s pointless: it could just as easily be written as a regular
function, and it would be more useful that way. double_sleep
is a
much more typical example: we have to make it async, because it calls
another async function. The end result is a kind of async sandwich,
with trio on both sides and our code in the middle:
trio.run -> double_sleep -> trio.sleep
This “sandwich” structure is typical for async code; in general, it looks like:
trio.run -> [async function] -> ... -> [async function] -> trio.whatever
It’s exactly the functions on the path between trio.run()
and
trio.whatever
that have to be async. Trio provides the async
bread, and then your code makes up the async sandwich’s tasty async
filling. Other functions (e.g., helpers you call along the way) should
generally be regular, non-async functions.
Warning: don’t forget that await
!¶
Now would be a good time to open up a Python prompt and experiment a
little with writing simple async functions and running them with
trio.run
.
At some point in this process, you’ll probably write some code like
this, that tries to call an async function but leaves out the
await
:
import time
import trio
async def broken_double_sleep(x):
print("*yawn* Going to sleep")
start_time = time.time()
# Whoops, we forgot the 'await'!
trio.sleep(2 * x)
sleep_time = time.time() - start_time
print("Woke up after {:.2f} seconds, feeling well rested!".format(sleep_time))
trio.run(broken_double_sleep, 3)
You might think that Python would raise an error here, like it does
for other kinds of mistakes we sometimes make when calling a
function. Like, if we forgot to pass trio.sleep()
it’s required
argument, then we would get a nice TypeError
saying so. But
unfortunately, if you forget an await
, you don’t get that. What
you actually get is:
>>> trio.run(broken_double_sleep, 3)
*yawn* Going to sleep
Woke up again after 0.00 seconds, feeling well rested!
__main__:4: RuntimeWarning: coroutine 'sleep' was never awaited
>>>
This is clearly broken – 0.00 seconds is not long enough to feel well
rested! Yet the code acts like it succeeded – no exception was
raised. The only clue that something went wrong is that it prints
RuntimeWarning: coroutine 'sleep' was never awaited
. Also, the
exact place where the warning is printed might vary, because it
depends on the whims of the garbage collector. If you’re using PyPy,
you might not even get a warning at all until the next GC collection
runs:
# On PyPy:
>>>> trio.run(broken_double_sleep, 3)
*yawn* Going to sleep
Woke up again after 0.00 seconds, feeling well rested!
>>>> # what the ... ?? not even a warning!
>>>> # but forcing a garbage collection gives us a warning:
>>>> import gc
>>>> gc.collect()
/home/njs/pypy-3.5-nightly/lib-python/3/importlib/_bootstrap.py:191: RuntimeWarning: coroutine 'sleep' was never awaited
if _module_locks.get(name) is wr: # XXX PyPy fix?
0
>>>>
(If you can’t see the warning above, try scrolling right.)
Forgetting an await
like this is an incredibly common
mistake. You will mess this up. Everyone does. And Python will not
help you as much as you’d hope 😞. The key thing to remember is: if
you see the magic words RuntimeWarning: coroutine '...' was never
awaited
, then this always means that you made the mistake of
leaving out an await
somewhere, and you should ignore all the
other error messages you see and go fix that first, because there’s a
good chance the other stuff is just collateral damage. I’m not even
sure what all that other junk in the PyPy output is. Fortunately I
don’t need to know, I just need to fix my function!
(“I thought you said you weren’t going to mention coroutines!” Yes, well, I didn’t mention coroutines, Python did. Take it up with Guido! But seriously, this is unfortunately a place where the internal implementation details do leak out a bit.)
Why does this happen? In Trio, every time we use await
it’s to
call an async function, and every time we call an async function we
use await
. But Python’s trying to keep its options open for other
libraries that are ahem a little less organized about things. So
while for our purposes we can think of await trio.sleep(...)
as a
single piece of syntax, Python thinks of it as two things: first a
function call that returns this weird “coroutine” object:
>>> trio.sleep(3)
<coroutine object sleep at 0x7f5ac77be6d0>
and then that object gets passed to await
, which actually runs the
function. So if you forget await
, then two bad things happen: your
function doesn’t actually get called, and you get a “coroutine” object
where you might have been expecting something else, like a number:
>>> async_double(3) + 1
TypeError: unsupported operand type(s) for +: 'coroutine' and 'int'
If you didn’t already mess this up naturally, then give it a try on
purpose: try writing some code with a missing await
, or an extra
await
, and see what you get. This way you’ll be prepared for when
it happens to you for real.
And remember: watch out for RuntimeWarning: coroutine '...' was
never awaited
; it means you need to find and fix your missing
await
.
Okay, let’s see something cool already¶
So now we’ve started using trio, but so far all we’ve learned to do is
write functions that print things and sleep for various lengths of
time. Interesting enough, but we could just as easily have done that
with time.sleep()
. async/await
is useless!
Well, not really. Trio has one more trick up its sleeve, that makes async functions more powerful than regular functions: it can run multiple async function at the same time. Here’s an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | # tasks-intro.py
import trio
async def child1():
print(" child1: started! sleeping now...")
await trio.sleep(1)
print(" child1: exiting!")
async def child2():
print(" child2: started! sleeping now...")
await trio.sleep(1)
print(" child2: exiting!")
async def parent():
print("parent: started!")
async with trio.open_nursery() as nursery:
print("parent: spawning child1...")
nursery.spawn(child1)
print("parent: spawning child2...")
nursery.spawn(child2)
print("parent: waiting for children to finish...")
# -- we exit the nursery block here --
print("parent: all done!")
trio.run(parent)
|
There’s a lot going on in here, so we’ll take it one step at a
time. In the first part, we define two async functions child1
and
child2
. These should look familiar from the last section:
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | async def child1():
print(" child1: started! sleeping now...")
await trio.sleep(1)
print(" child1: exiting!")
async def child2():
print(" child2: started! sleeping now...")
await trio.sleep(1)
print(" child2: exiting!")
async def parent():
print("parent: started!")
async with trio.open_nursery() as nursery:
print("parent: spawning child1...")
nursery.spawn(child1)
print("parent: spawning child2...")
nursery.spawn(child2)
print("parent: waiting for children to finish...")
# -- we exit the nursery block here --
print("parent: all done!")
trio.run(parent)
|
Next, we define parent
as an async function that’s going to call
child1
and child2
concurrently:
15 16 17 18 19 20 21 22 23 24 25 26 | async def parent():
print("parent: started!")
async with trio.open_nursery() as nursery:
print("parent: spawning child1...")
nursery.spawn(child1)
print("parent: spawning child2...")
nursery.spawn(child2)
print("parent: waiting for children to finish...")
# -- we exit the nursery block here --
print("parent: all done!")
|
It does this by using a mysterious async with
statement to create
a “nursery”, and then “spawns” child1
and child2
into the
nursery.
Let’s start with this async with
thing. It’s actually pretty
simple. In regular Python, a statement like with someobj: ...
instructs the interpreter to call someobj.__enter__()
at the
beginning of the block, and to call someobj.__exit__()
at the end
of the block. We call someobj
a “context manager”. An async
with
does exactly the same thing, except that where a regular
with
statement calls regular methods, an async with
statement
calls async methods: at the start of the block it does await
someobj.__aenter__()
and at that end of the block it does await
someobj.__aexit__()
. In this case we call someobj
an “async
context manager”. So in short: with
blocks are a shorthand for
calling some functions, and since with async/await Python now has two
kinds of functions, it also needs two kinds of with
blocks. That’s
all there is to it! If you understand async functions, then you
understand async with
.
Note
This example doesn’t use them, but while we’re here we might as
well mention the one other piece of new syntax that async/await
added: async for
. It’s basically the same idea as async
with
versus with
: An async for
loop is just like a
for
loop, except that where a for
loop does
iterator.__next__()
to fetch the next item, an async for
does await async_iterator.__anext__()
. Now you understand all
of async/await. Basically just remember that it involves making
sandwiches and sticking the word “async” in front of everything,
and you’ll do fine.
Now that we understand async with
, let’s look at parent
again:
15 16 17 18 19 20 21 22 23 24 25 26 | async def parent():
print("parent: started!")
async with trio.open_nursery() as nursery:
print("parent: spawning child1...")
nursery.spawn(child1)
print("parent: spawning child2...")
nursery.spawn(child2)
print("parent: waiting for children to finish...")
# -- we exit the nursery block here --
print("parent: all done!")
|
There are only 4 lines of code that really do anything here. On line
17, we use trio.open_nursery()
to get a “nursery” object, and
then inside the async with
block we call nursery.spawn
twice,
on lines 19 and 22. There are actually two ways to call an async
function: the first one is the one we already say, using await
async_fn()
; the new one is nursery.spawn(async_fn)
: it asks trio
to start running this async function, but then returns immediately
without waiting for the function to finish. So after our two calls to
nursery.spawn
, child1
and child2
are now running in the
background. And then at line 25, the commented line, we hit the end of
the async with
block, and the nursery’s __aexit__
function
runs. What this does is force parent
to stop here and wait for all
the children in the nursery to exit. This is why you have to use
async with
to get a nursery: it gives us a way to make sure that
the child calls can’t run away and get lost. One reason this is
important is that if there’s a bug or other problem in one of the
children, and it raises an exception, then it lets us propagate that
exception into the parent; in many other frameworks, exceptions like
this are just discarded. Trio never discards exceptions.
However – this is important! – the parent won’t see the exception
unless and until it reaches the end of the nursery’s async wait
block and runs the __aexit__
function. So remember: in trio,
parenting is a full-time job! Any given piece of code manage a nursery
– which means opening it, spawning some children, and then sitting in
__aexit__
to supervise them – or it can do actual work, but you
shouldn’t try to do both at the same time in the same function. If you
find yourself tempted to do some work in the parent, then spawn
another child and have it do the work. In trio, children are cheap.
Ok! Let’s try running it and see what we get:
parent: started!
parent: spawning child1...
parent: spawning child2...
parent: waiting for children to finish...
child2: started! sleeping now...
child1: started! sleeping now...
[... 1 second passes ...]
child1: exiting!
child2: exiting!
parent: all done!
(Your output might have the order of the “started” and/or “exiting” lines swapped compared to to mine.)
Notice that child1
and child2
both start together and then
both exit together, and that the whole program only takes 1 second to
run, even though we made two calls to trio.sleep(1)
, which should
take two seconds in total. So it looks like child1
and child2
really are running at the same time!
Now, if you’re familiar with programming using threads, this might look familiar – and that’s intentional. But it’s important to realize that there are no threads here. All of this is happening in a single thread. To remind ourselves of this, we use slightly different terminology: instead of spawning two “threads”, we say that we spawned two “tasks”. There are two differences between tasks and threads: (1) many tasks can take turns running on a single thread, and (2) with threads, the Python interpreter/operating system can switch which thread is running whenever they feel like it; with tasks, we can only switch at certain designated places we call “checkpoints”. In the next section, we’ll dig into what this means.
Task switching illustrated¶
The big idea behind async/await-based libraries like trio is to run lots of tasks simultaneously on a single thread by switching between them at appropriate places – so for example, if we’re implementing a web server, then one task could be sending an HTTP response at the same time as another task is waiting for new connections. If all you want to do is use trio, then you don’t need to understand all the nitty-gritty detail of how this switching works – but it’s very useful to have at least a general intuition about what trio is doing “under the hood” when your code is executing. To help build that intuition, let’s look more closely at how trio ran our example from the last section.
Fortunately, trio provides a rich set of tools for inspecting
and debugging your programs. Here we want to watch
trio.run()
at work, which we can do by writing a class we’ll
call Tracer
, which implements trio’s Instrument
interface. Its job is to log various events as they happen:
class Tracer(trio.abc.Instrument):
def before_run(self):
print("!!! run started")
def _print_with_task(self, msg, task):
# repr(task) is perhaps more useful than task.name in general,
# but in context of a tutorial the extra noise is unhelpful.
print("{}: {}".format(msg, task.name))
def task_spawned(self, task):
self._print_with_task("### new task spawned", task)
def task_scheduled(self, task):
self._print_with_task("### task scheduled", task)
def before_task_step(self, task):
self._print_with_task(">>> about to run one step of task", task)
def after_task_step(self, task):
self._print_with_task("<<< task step finished", task)
def task_exited(self, task):
self._print_with_task("### task exited", task)
def before_io_wait(self, timeout):
if timeout:
print("### waiting for I/O for up to {} seconds".format(timeout))
else:
print("### doing a quick check for I/O")
self._sleep_time = trio.current_time()
def after_io_wait(self, timeout):
duration = trio.current_time() - self._sleep_time
print("### finished I/O check (took {} seconds)".format(duration))
def after_run(self):
print("!!! run finished")
Then we re-run our example program from the previous section, but this
time we pass trio.run()
a Tracer
object:
trio.run(parent, instruments=[Tracer()])
This generates a lot of output, so we’ll go through it one step at a time.
First, there’s a bit of chatter while trio gets ready to run our
code. Most of this is irrelevant to us for now, but in the middle you
can see that trio has created a task for the __main__.parent
function, and “scheduled” it (i.e., made a note that it should be run
soon):
$ python3 tutorial/tasks-with-trace.py
!!! run started
### new task spawned: <init>
### task scheduled: <init>
### doing a quick check for I/O
### finished I/O check (took 1.1122087016701698e-05 seconds)
>>> about to run one step of task: <init>
### new task spawned: <call soon task>
### task scheduled: <call soon task>
### new task spawned: __main__.parent
### task scheduled: __main__.parent
<<< task step finished: <init>
### doing a quick check for I/O
### finished I/O check (took 6.4980704337358475e-06 seconds)
Once the initial housekeeping is done, trio starts running the
parent
function, and you can see parent
creating the two child
tasks. Then it hits the end of the async with
block, and pauses:
>>> about to run one step of task: __main__.parent
parent: started!
parent: spawning child1...
### new task spawned: __main__.child1
### task scheduled: __main__.child1
parent: spawning child2...
### new task spawned: __main__.child2
### task scheduled: __main__.child2
parent: waiting for children to finish...
<<< task step finished: __main__.parent
Control then goes back to trio.run()
, which logs a bit more
internal chatter:
>>> about to run one step of task: <call soon task>
<<< task step finished: <call soon task>
### doing a quick check for I/O
### finished I/O check (took 5.476875230669975e-06 seconds)
And then gives the two child tasks a chance to run:
>>> about to run one step of task: __main__.child2
child2 started! sleeping now...
<<< task step finished: __main__.child2
>>> about to run one step of task: __main__.child1
child1: started! sleeping now...
<<< task step finished: __main__.child1
Each task runs until it hits the call to trio.sleep()
, and then
suddenly we’re back in trio.run()
deciding what to run next. How
does this happen? The secret is that trio.run()
and
trio.sleep()
work together to make it happen: trio.sleep()
has access to some special magic that lets it pause its entire
callstack, so it sends a note to trio.run()
requesting to be
woken again after 1 second, and then suspends the task. And once the
task is suspended, Python gives control back to trio.run()
,
which decides what to do next. (If this sounds similar to the way that
generators can suspend execution by doing a yield
, then that’s not
a coincidence: inside the Python interpreter, there’s a lot of overlap
between the implementation of generators and async functions.)
Note
You might wonder whether you can mix-and-match primitives from
different async libraries. For example, could we use
trio.run()
together with asyncio.sleep()
? The answer is
no, we can’t, and the paragraph above explains why: the two sides
of our async sandwich have a private language they use to talk to
each other, and different libraries use different languages. So if
you try to call asyncio.sleep()
from inside a
trio.run()
, then trio will get very confused indeed and
probably blow up in some dramatic way.
Only async functions have access to the special magic for suspending a
task, so only async functions can cause the program to switch to a
different task. What this means if a call doesn’t have an await
on it, then you know that it can’t be a place where your task will
be suspended. This makes tasks much easier to reason about than
threads, because there are far fewer ways that tasks can be
interleaved with each other and stomp on each others’ state. (For
example, in trio a statement like a += 1
is always atomic – even
if a
is some arbitrarily complicated custom object!) Trio also
makes some further guarantees beyond that, but
that’s the big one.
And now you also know why parent
had to use an async with
to
open the nursery: if we had used a regular with
block, then it
wouldn’t have been able to pause at the end and wait for the children
to finish; we need our cleanup function to be async, which is exactly
what async with
gives us.
Now, back to our execution trace. To recap: at this point parent
is waiting on child1
and child2
, and both children are
sleeping. So trio.run()
checks its notes, and sees that there’s
nothing to be done until those sleeps finish – unless possibly some
external I/O event comes in. If that happened, then it might give us
something to do. Of course we aren’t doing any I/O here so it won’t
happen, but in other situations it could. So next it calls an
operating system primitive to put the whole process to sleep:
### waiting for I/O for up to 0.9999009938910604 seconds
And in fact no I/O does arrive, so one second later we wake up again,
and trio checks its notes again. At this point it checks the current
time, compares it to the notes that trio.sleep()
sent saying
when when the two child tasks should be woken up again, and realizes
that they’ve slept for long enough, so it schedules them to run soon:
### finished I/O check (took 1.0006483688484877 seconds)
### task scheduled: __main__.child1
### task scheduled: __main__.child2
And then the children get to run, and this time they run to
completion. Remember how parent
is waiting for them to finish?
Notice how parent
gets scheduled when the first child exits:
>>> about to run one step of task: __main__.child1
child1: exiting!
### task scheduled: __main__.parent
### task exited: __main__.child1
<<< task step finished: __main__.child1
>>> about to run one step of task: __main__.child2
child2 exiting!
### task exited: __main__.child2
<<< task step finished: __main__.child2
Then, after another check for I/O, parent
wakes up. The nursery
cleanup code notices that all its children have exited, and lets the
nursery block finish. And then parent
makes a final print and
exits:
### doing a quick check for I/O
### finished I/O check (took 9.045004844665527e-06 seconds)
>>> about to run one step of task: __main__.parent
parent: all done!
### task scheduled: <init>
### task exited: __main__.parent
<<< task step finished: __main__.parent
And finally, after a bit more internal bookkeeping, trio.run()
exits too:
### doing a quick check for I/O
### finished I/O check (took 5.996786057949066e-06 seconds)
>>> about to run one step of task: <init>
### task scheduled: <call soon task>
### task scheduled: <init>
<<< task step finished: <init>
### doing a quick check for I/O
### finished I/O check (took 6.258022040128708e-06 seconds)
>>> about to run one step of task: <call soon task>
### task exited: <call soon task>
<<< task step finished: <call soon task>
>>> about to run one step of task: <init>
### task exited: <init>
<<< task step finished: <init>
!!! run finished
You made it!
That was a lot of text, but again, you don’t need to understand everything here to use trio – in fact, trio goes to great lengths to make each task feel like it executes in a simple, linear way. (Just like your operating system goes to great lengths to make it feel like your single-threaded code executes in a simple linear way, even though under the covers the operating system juggles between different threads and processes in essentially the same way trio does.) But it is useful to have a rough model in your head of how the code you write is actually executed, and – most importantly – the consequences of that for parallelism.
Alternatively, if this has just whetted your appetite and you want to
know more about how async/await
works internally, then this blog
post
is a good deep dive, or check out this great walkthrough to see
how to build a simple async I/O framework from the ground up.
A kinder, gentler GIL¶
Speaking of parallelism – let’s zoom out for a moment and talk about how async/await compares to other ways of handling concurrency in Python.
As we’ve already noted, trio tasks are conceptually rather similar to
Python’s built-in threads, as provided by the threading
module. And in all common Python implementations, threads have a
famous limitation: the Global Interpreter Lock, or “GIL” for
short. The GIL means that even if you use multiple threads, your code
still (mostly) ends up running on a single core. People tend to find
this frustrating.
But from trio’s point of view, the problem with the GIL isn’t that it restricts parallelism. Of course it would be nice if Python had better options for taking advantage of multiple cores, but that’s an extremely difficult problem to solve, and in the mean time there are lots of problems where a single core is totally adequate – or where if it isn’t, then process- or machine-level parallelism works fine.
No, the problem with the GIL is that it’s a lousy deal: we give up on using multiple cores, and in exchange we get... almost all the same challenges and mind bending bugs that come with real parallel programming, and – to add insult to injury – pretty poor scalability. Threads in Python just aren’t that appealing.
Trio doesn’t make your code run on multiple cores; in fact, as we saw
above, it’s baked into trio’s design that you never have two tasks
running at the same time. We’re not so much overcoming the GIL as
embracing it. But if you’re willing to accept that, plus a bit of
extra work to put these new async
and await
keywords in the
right places, then in exchange you get:
- Excellent scalability: trio can run 10,000+ tasks simultaneously without breaking a sweat, so long as their total CPU demands don’t exceed what a single core can provide. (This is common in, for example, network servers that have lots of clients connected, but only a few active at any given time.)
- Fancy features: most threading systems are implemented in C and restricted to whatever features the operating system provides. In trio our logic is all in Python, which makes it possible to implement powerful and ergonomic features like trio’s cancellation system.
- Code that’s easier to reason about: the
await
keyword means that potential task-switching points are explicitly marked within each function. This can make trio code dramatically easier to reason about than the equivalent program using threads.
Certainly it’s not appropriate for every app... but there are a lot of situations where the trade-offs here look pretty appealing.
There is one downside that’s important to keep in mind, though. Making checkpoints explicit gives you more control over how your tasks can be interleaved – but with great power comes great responsibility. With threads, the runtime environment is responsible for making sure that each thread gets its fair share of running time. With trio, if some task runs off and does stuff for seconds on end without executing a checkpoint, then... all your other tasks will just have to wait.
Here’s an example of how this can go wrong. Take our example
from above, and replace the calls to
trio.sleep()
with calls to time.sleep()
. If we run our
modified program, we’ll see something like:
parent: started!
parent: spawning child1...
parent: spawning child2...
parent: waiting for children to finish...
child2 started! sleeping now...
[... pauses for 1 second ...]
child2 exiting!
child1: started! sleeping now...
[... pauses for 1 second ...]
child1: exiting!
parent: all done!
One of the major reasons why trio has such a rich instrumentation API is to make it possible to write debugging tools to catch issues like this.
Networking with trio¶
Now let’s take what we’ve learned and use it to do some I/O, which is where async/await really shines.
An echo client: low-level API¶
The traditional application for demonstrating network APIs is an “echo server”: a program that accepts arbitrary data from a client, and then sends that same data right back. Probably a more relevant example these days would be an application that does lots of concurrent HTTP requests, but trio doesn’t have an HTTP library yet, so we’ll stick with the echo server tradition.
To start with, here’s an example echo client, i.e., the program that will send some data at our echo server and get responses back:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | # echo-client-low-level.py
import sys
import trio
# arbitrary, but:
# - must be in between 1024 and 65535
# - can't be in use by some other program on your computer
# - must match what we set in our echo client
PORT = 12345
# How much memory to spend (at most) on each call to recv. Pretty arbitrary,
# but shouldn't be too big or too small.
BUFSIZE = 16384
async def sender(client_sock):
print("sender: started!")
while True:
data = b"async can sometimes be confusing, but I believe in you!"
print("sender: sending {!r}".format(data))
await client_sock.sendall(data)
await trio.sleep(1)
async def receiver(client_sock):
print("receiver: started!")
while True:
data = await client_sock.recv(BUFSIZE)
print("receiver: got data {!r}".format(data))
if not data:
print("receiver: connection closed")
sys.exit()
async def parent():
print("parent: connecting to 127.0.0.1:{}".format(PORT))
with trio.socket.socket() as client_sock:
await client_sock.connect(("127.0.0.1", PORT))
async with trio.open_nursery() as nursery:
print("parent: spawning sender...")
nursery.spawn(sender, client_sock)
print("parent: spawning receiver...")
nursery.spawn(receiver, client_sock)
trio.run(parent)
|
The overall structure here should be familiar, because it’s just like
our last example: we have a
parent task, which spawns two child tasks to do the actual work, and
then at the end of the async with
block it switches into full-time
parenting mode while waiting for them to finish. But now instead of
just calling trio.sleep()
, the children use some of trio’s
networking APIs.
Let’s look at the parent first:
32 33 34 35 36 37 38 39 40 41 | async def parent():
print("parent: connecting to 127.0.0.1:{}".format(PORT))
with trio.socket.socket() as client_sock:
await client_sock.connect(("127.0.0.1", PORT))
async with trio.open_nursery() as nursery:
print("parent: spawning sender...")
nursery.spawn(sender, client_sock)
print("parent: spawning receiver...")
nursery.spawn(receiver, client_sock)
|
We’re using the trio.socket
API to access network
functionality. (If you know the socket
module in the standard
library, then trio.socket
is very similar, just asyncified.)
First we call trio.socket.socket()
to create the socket object
we’ll use to connect to the server, and we use a with
block to
make sure that it will be closed properly. (Trio is designed around
the assumption that you’ll be using with
blocks to manage resource
cleanup – highly recommended!) Then we call connect
to connect to
the echo server. 127.0.0.1
is a magic IP address meaning “the computer
I’m running on”, so (127.0.0.1, PORT)
means that we want to
connect to whatever program on the current computer is using PORT
as its contact point. And then once the connection is made, we pass
the connected client socket into the two child tasks. (This is also a
good example of how nursery.spawn
lets you pass positional
arguments to the spawned function.)
Our first task’s job is to send data to the server:
15 16 17 18 19 20 21 | async def sender(client_sock):
print("sender: started!")
while True:
data = b"async can sometimes be confusing, but I believe in you!"
print("sender: sending {!r}".format(data))
await client_sock.sendall(data)
await trio.sleep(1)
|
It uses a loop that alternates between calling await
client_sock.sendall(...)
to send some data, and then sleeping for a
second to avoid making the output scroll by too fast on your terminal.
And the second task’s job is to process the data the server sends back:
23 24 25 26 27 28 29 30 | async def receiver(client_sock):
print("receiver: started!")
while True:
data = await client_sock.recv(BUFSIZE)
print("receiver: got data {!r}".format(data))
if not data:
print("receiver: connection closed")
sys.exit()
|
It repeatedly calls await client_sock.recv(...)
to get more data
from the server, and then checks to see if the server has closed the
connection. recv
only returns an empty bytestring if the
connection has been closed; if there’s no data available, then it
blocks until more data arrives.
And now we’re ready to look at the server.
An echo server: low-level API¶
The server is a little trickier. As usual, let’s look at the whole thing, and then we’ll discuss the pieces:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | # echo-server-low-level.py
import trio
# Port is arbitrary, but:
# - must be in between 1024 and 65535
# - can't be in use by some other program on your computer
# - must match what we set in our echo client
PORT = 12345
# How much memory to spend (at most) on each call to recv. Pretty arbitrary,
# but shouldn't be too big or too small.
BUFSIZE = 16384
async def echo_server(server_sock, ident):
with server_sock:
print("echo_server {}: started".format(ident))
try:
while True:
data = await server_sock.recv(BUFSIZE)
print("echo_server {}: received data {!r}".format(ident, data))
if not data:
print("echo_server {}: connection closed".format(ident))
return
print("echo_server {}: sending data {!r}".format(ident, data))
await server_sock.sendall(data)
except Exception as exc:
# Unhandled exceptions will propagate into our parent and take
# down the whole program. If the exception is KeyboardInterrupt,
# that's what we want, but otherwise maybe not...
print("echo_server {}: crashed: {!r}".format(ident, exc))
async def echo_listener(nursery):
with trio.socket.socket() as listen_sock:
# Notify the operating system that we want to receive connection
# attempts at this address:
listen_sock.bind(("127.0.0.1", PORT))
listen_sock.listen()
print("echo_listener: listening on 127.0.0.1:{}".format(PORT))
ident = 0
while True:
server_sock, _ = await listen_sock.accept()
print("echo_listener: got new connection, spawning echo_server")
ident += 1
nursery.spawn(echo_server, server_sock, ident)
async def parent():
async with trio.open_nursery() as nursery:
print("parent: spawning echo_listener")
nursery.spawn(echo_listener, nursery)
trio.run(parent)
|
The actual echo server implementation should be fairly familiar at
this point. Each incoming connection from an echo client gets handled
by its own dedicated task, running the echo_server
function:
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | async def echo_server(server_sock, ident):
with server_sock:
print("echo_server {}: started".format(ident))
try:
while True:
data = await server_sock.recv(BUFSIZE)
print("echo_server {}: received data {!r}".format(ident, data))
if not data:
print("echo_server {}: connection closed".format(ident))
return
print("echo_server {}: sending data {!r}".format(ident, data))
await server_sock.sendall(data)
except Exception as exc:
# Unhandled exceptions will propagate into our parent and take
# down the whole program. If the exception is KeyboardInterrupt,
# that's what we want, but otherwise maybe not...
print("echo_server {}: crashed: {!r}".format(ident, exc))
|
We take a socket object that’s connected to the client (so the data we
pass to sendall
on the client comes out of recv
here, and
vice-versa), plus ident
which is just a unique number used to make
the print output less confusing when there are multiple clients
connected at the same time. Then we have our usual with
block to
make sure the socket gets closed, a try
block discussed below, and
finally the server loop which alternates between reading some data
from the socket and then sending it back out again (unless the socket
was closed, in which case we quit).
Remember that in trio, like Python in general, exceptions keep
propagating until they’re caught. Here we think it’s plausible there
might be unexpected exceptions, and we want to isolate that to making
just this one task crash, without taking down the whole program. For
example, if the client closes the connection at the wrong moment then
it’s possible this code will end up calling sendall
on a closed
connection and get an OSError
; that’s unfortunate, and in a
more serious program we might want to handle it more explicitly, but
it doesn’t indicate a problem for any other connections. On the other
hand, if the exception is something like a KeyboardInterrupt
,
we do want that to propagate out into the parent task and cause the
program to exit. To express this, we use a try
block with an
except Exception:
handler.
But where do these echo_server
tasks come from? An important part
of writing a trio program is deciding how you want to organize your
tasks. In the examples we’ve seen so far, this was simple, because the
set of tasks was fixed. Here, we want to wait for clients to connect,
and then spawn a new task for each one. The tricky part is that like
we mentioned above, managing a nursery is a full time job: you don’t
want the task that has the nursery and is supervising the child tasks
to do anything else, like listen for new connections.
There’s a standard trick for handling this in trio: our parent task creates a nursery, spawns a child task to listen for new connections, and then passes the nursery object to the child task:
47 48 49 50 | async def parent():
async with trio.open_nursery() as nursery:
print("parent: spawning echo_listener")
nursery.spawn(echo_listener, nursery)
|
Now echo_listener
can spawn “siblings” instead of children – even
though the echo_listener
is the one spawning echo_server
tasks, we end up with a task tree that looks like:
parent
│
├─ echo_listener
│
├─ echo_server 1
│
├─ echo_server 2
┆
This lets parent
focus on supervising the children,
echo_listener
focus on listening for new connections, each
echo_server
call will handle a single client.
Once we know this trick, the listener code becomes pretty straightforward:
32 33 34 35 36 37 38 39 40 41 42 43 44 45 | async def echo_listener(nursery):
with trio.socket.socket() as listen_sock:
# Notify the operating system that we want to receive connection
# attempts at this address:
listen_sock.bind(("127.0.0.1", PORT))
listen_sock.listen()
print("echo_listener: listening on 127.0.0.1:{}".format(PORT))
ident = 0
while True:
server_sock, _ = await listen_sock.accept()
print("echo_listener: got new connection, spawning echo_server")
ident += 1
nursery.spawn(echo_server, server_sock, ident)
|
We create a listen socket, start it listening, and then go into an
infinite loop, accepting connections from clients and spawning an
echo_server
task to handle each one.
We don’t expect there to be any errors here in the listener code – if
there are, it’s probably a bug, and probably means that our whole
program is broken (a server that doesn’t accept connections isn’t very
useful!). So we don’t have a catch-all try
block here. In general,
trio leaves it up to you to decide whether and how you want to handle
exceptions.
Try it out¶
Open a few terminals, run echo-server-low-level.py
in one, run
echo-client-low-level.py
in another, and watch the messages scroll
by! When you get bored, you can exit by hitting control-C.
Some things to try:
- Open another terminal, and run 2 clients at the same time.
- See how the server reacts when you hit control-C on the client
- See how the client reacts when you hit control-C on the server
Flow control in our echo client and server¶
Here’s a question you might be wondering about: why does our client use two separate tasks for sending and receiving, instead of a single task that alternates between them – like the server has? For example, our client could use a single task like:
# Can you spot the two problems with this code?
async def send_and_receive(client_sock):
while True:
data = ...
await client_sock.sendall(data)
received = await client_sock.recv(BUFSIZE)
if not received:
sys.exit()
await trio.sleep(1)
It turns out there are two problems with this – one minor and one
major. Both relate to flow control. The minor problem is that when we
call recv
here we’re not waiting for all the data to be
available; recv
returns as soon as any data is available. If
data
is small, then our operating systems / network / server will
probably keep it all together in a single chunk, but there’s no
guarantee. If the server sends hello
then we might get hello
,
or hel
lo
, or h
e
l
l
o
, or ... bottom
line, any time we’re expecting more than one byte of data, we have to
be prepared to call recv
multiple times.
And where this would go especially wrong is if we find ourselves in
the situation where len(data) > BUFSIZE
. On each pass through the
loop, we send len(data)
bytes, but only read at most BUFSIZE
bytes. The result is something like a memory leak: we’ll end up with
more and more data backed up in the network, until eventually
something breaks.
We could fix this by keeping track of how much data we’re expecting at
each moment, and then keep calling recv
until we get it all:
expected = len(data)
while expected > 0:
received = await client_sock.recv(BUFSIZE)
if not received:
sys.exit(1)
expected -= len(received)
This is a bit cumbersome, but it would solve this problem.
There’s another problem, though, that’s deeper. We’re still
alternating between sending and receiving. Notice that when we send
data, we use await
: this means that sending can potentially
block. Why does this happen? Any data that we send goes first into
an operating system buffer, and from there onto the network, and then
another operating system buffer on the receiving computer, before the
receiving program finally calls recv
to take the data out of these
buffers. If we call sendall
with a small amount of data, then it
goes into these buffers and sendall
returns immediately. But if we
send enough data fast enough, eventually the buffers fill up, and
sendall
will block until the remote side calls recv
and frees
up some space.
Now let’s think about this from the server’s point of view. Each time
it calls recv
, it gets some data that it needs to send back. And
until it sends it back, the data is sitting around takes up
memory. Computers have finite amounts of RAM, so if our server is well
behaved then at some point it needs to stop calling recv
until
it gets rid of some of the old data by doing its own call to
sendall
. So for the server, really the only viable option is to
alternate between receiving and sending.
But we need to remember that it’s not just the client’s call to
sendall
that might block: the server’s call to sendall
can
also get into a situation where it blocks until the client calls
recv
. So if the server is waiting for sendall
to finish before
it calls recv
, and our client also waits for sendall
to finish
before it calls recv
,... we have a problem! The client won’t call
recv
until the server has called recv
, and the server won’t
call recv
until the client has called recv
. If our client is
written to alternate between sending and receiving, and the chunk of
data it’s trying to send is large enough (e.g. 10 megabytes will
probably do it in most configurations), then the two processes will
deadlock.
Moral: trio gives you powerful tools to manage sequential and
concurrent execution. In this example we saw that the server needs
send
and recv
to alternate in sequence, while the client needs
them to run concurrently, and both were straightforward to
implement. But when you’re implementing network code like this then
it’s important to think carefully about flow control and buffering,
because it’s up to you to choose the right execution mode!
Other popular async libraries like Twisted and asyncio
tend to paper over
these kinds of issues by throwing in unbounded buffers
everywhere. This can avoid deadlocks, but can introduce its own
problems and in particular can make it difficult to keep memory usage
and latency under control. While
both approaches have their advantages, trio takes the position that
it’s better to expose the underlying problem as directly as possible
and provide good tools to confront it head-on.
Note
If you want to try and make the deadlock happen on purpose to see
for yourself, and you’re using Windows, then you might need to
split the sendall
call up into two calls that each send half of
the data. This is because Windows has a somewhat unusual way of
handling buffering.
An echo client and server: higher-level API¶
TODO: Not implemented yet!
When things go wrong: timeouts, cancellation and exceptions in concurrent tasks¶
TODO: give an example using fail_after()
TODO: explain Cancelled
TODO: explain how cancellation is also used when one child raises an exception
TODO: show an example MultiError
traceback and walk through its
structure
TODO: maybe a brief discussion of KeyboardInterrupt
handling?
Trio’s core functionality¶
Entering trio¶
If you want to use trio, then the first thing you have to do is call
trio.run()
:
-
trio.
run
(async_fn, *args, clock=None, instruments=[], restrict_keyboard_interrupt_to_checkpoints=False)¶ Run a trio-flavored async function, and return the result.
Calling:
run(async_fn, *args)
is the equivalent of:
await async_fn(*args)
except that
run()
can (and must) be called from a synchronous context.This is trio’s main entry point. Almost every other function in trio requires that you be inside a call to
run()
.Parameters: - async_fn – An async function.
- args – Positional arguments to be passed to async_fn. If you need to
pass keyword arguments, then use
functools.partial()
. - clock –
None
to use the default system-specific monotonic clock; otherwise, an object implementing thetrio.abc.Clock
interface, like (for example) atrio.testing.MockClock
instance. - instruments (list of
trio.abc.Instrument
objects) – Any instrumentation you want to apply to this run. This can also be modified during the run; see Debugging and instrumentation. - restrict_keyboard_interrupt_to_checkpoints (bool) –
What happens if the user hits control-C while
run()
is running? If this argument is False (the default), then you get the standard Python behavior: aKeyboardInterrupt
exception will immediately interrupt whatever task is running (or if no task is running, then trio will wake up a task to be interrupted). Alternatively, if you set this argument to True, thenKeyboardInterrupt
delivery will be delayed: it will be only be raised at checkpoints, like aCancelled
exception.The default behavior is nice because it means that even if you accidentally write an infinite loop that never executes any checkpoints, then you can still break out of it using control-C. The the alternative behavior is nice if you’re paranoid about a
KeyboardInterrupt
at just the wrong place leaving your program in an inconsistent state, because it means that you only have to worry aboutKeyboardInterrupt
at the exact same places where you already have to worry aboutCancelled
.This setting has no effect if your program has registered a custom SIGINT handler, or if
run()
is called from anywhere but the main thread (this is a Python limitation), or if you usecatch_signals()
to catch SIGINT.
Returns: Whatever
async_fn
returns.Raises: TrioInternalError
– if an unexpected error is encountered inside trio’s internal machinery. This is a bug and you should let us know.- Anything else – if
async_fn
raises an exception, thenrun()
propagates it.
General principles¶
Checkpoints¶
When writing code using trio, it’s very important to understand the concept of a checkpoint. Many of trio’s functions act as checkpoints.
A checkpoint is two things:
- It’s a point where trio checks for cancellation. For example, if
the code that called your function set a timeout, and that timeout
has expired, then the next time your function executes a checkpoint
trio will raise a
Cancelled
exception. See Cancellation and timeouts below for more details. - It’s a point where the trio scheduler checks its scheduling policy to see if it’s a good time to switch to another task, and potentially does so. (Currently, this check is very simple: the scheduler always switches at every checkpoint. But this might change in the future.)
When writing trio code, you need to keep track of where your
checkpoints are. Why? First, because checkpoints require extra
scrutiny: whenever you execute a checkpoint, you need to be prepared
to handle a Cancelled
error, or for another task to run and
rearrange some state out from under you. And
second, because you also need to make sure that you have enough
checkpoints: if your code doesn’t pass through a checkpoint on a
regular basis, then it will be slow to notice and respond to
cancellation and – much worse – since trio is a cooperative
multi-tasking system where the only place the scheduler can switch
tasks is at checkpoints, it’ll also prevent the scheduler from fairly
allocating time between different tasks and adversely effect the
response latency of all the other code running in the same
process. (Informally we say that a task that does this is “hogging the
run loop”.)
So when you’re doing code review on a project that uses trio, one of the things you’ll want to think about is whether there are enough checkpoints, and whether each one is handled correctly. Of course this means you need a way to recognize checkpoints. How do you do that? The underlying principle is that any operation that blocks has to be a checkpoint. This makes sense: if an operation blocks, then it might block for a long time, and you’ll want to be able to cancel it if a timeout expires; and in any case, while this task is blocked we want another task to be scheduled to run so our code can make full use of the CPU.
But if we want to write correct code in practice, then this principle is a little too sloppy and imprecise to be useful. How do we know which functions might block? What if a function blocks sometimes, but not others, depending on the arguments passed / network speed / phase of the moon? How do we figure out where the checkpoints are when we’re stressed and sleep deprived but still want to get this code review right, and would prefer to reserve our mental energy for thinking about the actual logic instead of worrying about check points?
Don’t worry – trio’s got your back. Since checkpoints are important and ubiquitous, we make it as simple as possible to keep track of them. Here are the rules:
Regular (synchronous) functions never contain any checkpoints.
Every async function provided by trio always acts as a check point; if you see
await <something in trio>
, orasync for ... in <a trio object>
, orasync with <trio.something>
, then that’s definitely a checkpoint.(Partial exception: for async context managers, it might be only the entry or only the exit that acts as a checkpoint; this is documented on a case-by-case basis.)
Third-party async functions can act as checkpoints; if you see
await <something>
or one of its friends, then that might be a checkpoint. So to be safe, you should prepare for scheduling or cancellation happening there.
The reason we distinguish between trio functions and other functions is that we can’t make any guarantees about third party code. Checkpoint-ness is a transitive property: if function A acts as a checkpoint, and you write a function that calls function A, then your function also acts as a checkpoint. If you don’t, then it isn’t. So there’s nothing stopping someone from writing a function like:
# technically legal, but bad style:
async def why_is_this_async():
return 7
that never calls any of trio’s async functions. This is an async function, but it’s not a checkpoint. But why make a function async if it never calls any async functions? It’s possible, but it’s a bad idea. If you have a function that’s not calling any async functions, then you should make it synchronous. The people who use your function will thank you, because it makes it obvious that your function is not a checkpoint, and their code reviews will go faster.
(Remember how in the tutorial we emphasized the importance of the
“async sandwich”, and the way it means that
await
ends up being a marker that shows when you’re calling a
function that calls a function that ... eventually calls one of trio’s
built-in async functions? The transitivity of async-ness is a
technical requirement that Python imposes, but since it exactly
matches the transitivity of checkpoint-ness, we’re able to exploit it
to help you keep track of checkpoints. Pretty sneaky, eh?)
A slightly trickier case is a function like:
async def sleep_or_not(should_sleep):
if should_sleep:
await trio.sleep(1)
else:
pass
Here the function acts as a checkpoint if you call it with
should_sleep
set to a true value, but not otherwise. This is why
we emphasize that trio’s own async functions are unconditional check
points: they always check for cancellation and check for scheduling,
regardless of what arguments they’re passed. If you find an async
function in trio that doesn’t follow this rule, then it’s a bug and
you should let us know.
Inside trio, we’re very picky about this, because trio is the foundation of the whole system so we think it’s worth the extra effort to make things extra predictable. It’s up to you how picky you want to be in your code. To give you a more realistic example of what this kind of issue looks like in real life, consider this function:
async def recv_exactly(sock, nbytes):
data = bytearray()
while nbytes > 0:
# SocketType.recv() reads up to 'nbytes' bytes each time
chunk += await sock.recv(nbytes)
if not chunk:
raise RuntimeError("socket unexpected closed")
nbytes -= len(chunk)
data += chunk
return data
If called with an nbytes
that’s greater than zero, then it will
call sock.recv
at least once, and recv
is an async trio
function, and thus an unconditional checkpoint. So in this case,
recv_exactly
acts as a checkpoint. But if we do await
recv_exactly(sock, 0)
, then it will immediately return an empty
buffer without executing a checkpoint. If this were a function in
trio itself, then this wouldn’t be acceptable, but you may decide you
don’t want to worry about this kind of minor edge case in your own
code.
If you do want to be careful, or if you have some CPU-bound code that
doesn’t have enough checkpoints in it, then it’s useful to know that
await trio.sleep(0)
is an idiomatic way to execute a checkpoint
without doing anything else, and that
trio.testing.assert_yields()
can be used to test that an
arbitrary block of code contains a checkpoint.
Thread safety¶
The vast majority of trio’s API is not thread safe: it can only be
used from inside a call to trio.run()
. This manual doesn’t
bother documenting this on individual calls; unless specifically noted
otherwise, you should assume that it isn’t safe to call any trio
functions from anywhere except the trio thread. (But see below if you really do need to work with threads.)
Time and clocks¶
Every call to run()
has an associated clock.
By default, trio uses an unspecified monotonic clock, but this can be
changed by passing a custom clock object to run()
(e.g. for
testing).
You should not assume that trio’s internal clock matches any other
clock you have access to, including the clocks of simultaneous calls
to trio.run()
happening in other processes or threads!
The default clock is currently implemented as time.monotonic()
plus a large random offset. The idea here is to catch code that
accidentally uses time.monotonic()
early, which should help keep
our options open for changing the clock implementation later, and (more importantly)
make sure you can be confident that custom clocks like
trio.testing.MockClock
will work with third-party libraries
you don’t control.
-
trio.
current_time
()¶ Returns the current time according to trio’s internal clock.
Returns: The current time. Return type: float Raises: RuntimeError
– if not inside a call totrio.run()
.
-
await
trio.
sleep
(seconds)¶ Pause execution of the current task for the given number of seconds.
Parameters: seconds (float) – The number of seconds to sleep. May be zero to insert a checkpoint without actually blocking. Raises: ValueError
– if seconds is negative.
-
await
trio.
sleep_until
(deadline)¶ Pause execution of the current task until the given time.
The difference between
sleep()
andsleep_until()
is that the former takes a relative time and the latter takes an absolute time.Parameters: deadline (float) – The time at which we should wake up again. May be in the past, in which case this function yields but does not block.
-
await
trio.
sleep_forever
()¶ Pause execution of the current task forever (or until cancelled).
Equivalent to calling
await sleep(math.inf)
.
If you’re a mad scientist or otherwise feel the need to take direct
control over the PASSAGE OF TIME ITSELF, then you can implement a
custom Clock
class:
-
class
trio.abc.
Clock
¶ The interface for custom run loop clocks.
-
abstractmethod
start_clock
()¶ Do any setup this clock might need.
Called at the beginning of the run.
-
abstractmethod
current_time
()¶ Return the current time, according to this clock.
This is used to implement functions like
trio.current_time()
andtrio.move_on_after()
.Returns: The current time. Return type: float
-
abstractmethod
deadline_to_sleep_time
(deadline)¶ Compute the real time until the given deadline.
This is called before we enter a system-specific wait function like :func:~select.select`, to get the timeout to pass.
For a clock using wall-time, this should be something like:
return deadline - self.current_time()
but of course it may be different if you’re implementing some kind of virtual clock.
Parameters: deadline (float) – The absolute time of the next deadline, according to this clock. Returns: The number of real seconds to sleep until the given deadline. May be math.inf
.Return type: float
-
abstractmethod
You can also fetch a reference to the current clock, which might be useful if you’re using a custom clock class:
Cancellation and timeouts¶
Trio has a rich, composable system for cancelling work, either explicitly or when a timeout expires.
A simple timeout example¶
In the simplest case, you can apply a timeout to a block of code:
with trio.move_on_after(30):
result = await do_http_get("https://...")
print("result is", result)
print("with block finished")
We refer to move_on_after()
as creating a “cancel scope”, which
contains all the code that runs inside the with
block. If the HTTP
request takes more than 30 seconds to run, then it will be cancelled:
we’ll abort the request and we won’t see result is ...
printed
on the console; instead we’ll go straight to printing the with block
finished
message.
Note
Note that this is a single 30 second timeout for the entire body of
the with
statement. This is different from what you might have
seen with other Python libraries, where timeouts often refer to
something more complicated. We
think this way is easier to reason about.
How does this work? There’s no magic here: trio is built using
ordinary Python functionality, so we can’t just abandon the code
inside the with
block. Instead, we take advantage of Python’s
standard way of aborting a large and complex piece of code: we raise
an exception.
Here’s the idea: whenever you call a cancellable function like await
trio.sleep(...)
or await sock.recv(...)
– see Checkpoints
– then the first thing that function does is to check if there’s a
surrounding cancel scope whose timeout has expired, or otherwise been
cancelled. If so, then instead of performing the requested operation,
the function fails immediately with a Cancelled
exception. In
this example, this probably happens somewhere deep inside the bowels
of do_http_get
. The exception then propagates out like any normal
exception (you could even catch it if you wanted, but that’s generally
a bad idea), until it reaches the with move_on_after(...):
. And at
this point, the Cancelled
exception has done its job – it’s
successfully unwound the whole cancelled scope – so
move_on_after()
catches it, and execution continues as normal
after the with
block. And this all works correctly even if you
have nested cancel scopes, because every Cancelled
object
carries an invisible marker that makes sure that the cancel scope that
triggered it is the only one that will catch it.
Handling cancellation¶
Pretty much any code you write using trio needs to have some strategy
to handle Cancelled
exceptions – even if you didn’t set a
timeout, then your caller might (and probably will).
You can catch Cancelled
, but you shouldn’t! Or more precisely,
if you do catch it, then you should do some cleanup and then re-raise
it or otherwise let it continue propagating (unless you encounter an
error, in which case it’s OK to let that propagate instead). To help
remind you of this fact, Cancelled
inherits from
BaseException
, like KeyboardInterrupt
and
SystemExit
do, so that it won’t be caught by catch-all except
Exception:
blocks.
It’s also important in any long-running code to make sure that you
regularly check for cancellation, because otherwise timeouts won’t
work! This happens implicitly every time you call a cancellable
operation; see below for details. If
you have a task that has to do a lot of work without any I/O, then you
can use await sleep(0)
to insert an explicit cancel+schedule
point.
Here’s a rule of thumb for designing good trio-style (“trionic”?)
APIs: if you’re writing a reusable function, then you shouldn’t take a
timeout=
parameter, and instead let your caller worry about
it. This has several advantages. First, it leaves the caller’s options
open for deciding how they prefer to handle timeouts – for example,
they might find it easier to work with absolute deadlines instead of
relative timeouts. If they’re the ones calling into the cancellation
machinery, then they get to pick, and you don’t have to worry about
it. Second, and more importantly, this makes it easier for others to
re-use your code. If you write a http_get
function, and then I
come along later and write a log_in_to_twitter
function that needs
to internally make several http_get
calls, I don’t want to have to
figure out how to configure the individual timeouts on each of those
calls – and with trio’s timeout system, it’s totally unnecessary.
Of course, this rule doesn’t apply to APIs that need to impose
internal timeouts. For example, if you write a start_http_server
function, then you probably should give your caller some way to
configure timeouts on individual requests.
Cancellation semantics¶
You can freely nest cancellation blocks, and each Cancelled
exception “knows” which block it belongs to. So long as you don’t stop
it, the exception will keep propagating until it reaches the block
that raised it, at which point it will stop automatically.
Here’s an example:
print("starting...")
with trio.move_on_after(5):
with trio.move_on_after(10):
await sleep(20)
print("sleep finished without error")
print("move_on_after(10) finished without error")
print("move_on_after(5) finished without error")
In this code, the outer scope will expire after 5 seconds, causing the
sleep()
call to return early with a Cancelled
exception. Then this exception will propagate through the with
move_on_after(10)
line until it’s caught by the with
move_on_after(5)
context manager. So this code will print:
starting...
move_on_after(5) finished without error
The end result is that trio has successfully cancelled exactly the work that was happening within the scope that was cancelled.
Looking at this, you might wonder how you can tell whether the inner
block timed out – perhaps you want to do something different, like try
a fallback procedure or report a failure to our caller. To make this
easier, move_on_after()
‘s __enter__
function returns an
object representing this cancel scope, which we can use to check
whether this scope caught a Cancelled
exception:
with trio.move_on_after(5) as cancel_scope:
await sleep(10)
print(cancel_scope.cancelled_caught) # prints "True"
The cancel_scope
object also allows you to check or adjust this
scope’s deadline, explicitly trigger a cancellation without waiting
for the deadline, check if the scope has already been cancelled, and
so forth – see open_cancel_scope()
below for the full details.
Cancellations in trio are “level triggered”, meaning that once a block
has been cancelled, all cancellable operations in that block will
keep raising Cancelled
. This helps avoid some pitfalls around
resource clean-up. For example, imagine that we have a function that
connects to a remote server and sends some messages, and then cleans
up on the way out:
with trio.move_on_after(TIMEOUT):
conn = make_connection()
try:
await conn.send_hello_msg()
finally:
await conn.send_goodbye_msg()
Now suppose that the remote server stops responding, so our call to
await conn.send_hello_msg()
hangs forever. Fortunately, we were
clever enough to put a timeout around this code, so eventually the
timeout will expire and send_hello_msg
will raise
Cancelled
. But then, in the finally
block, we make another
blocking operation, which will also hang forever! At this point, if we
were using asyncio
or another library with “edge-triggered”
cancellation, we’d be in trouble: since our timeout already fired, it
wouldn’t fire again, and at this point our application would lock up
forever. But in trio, this doesn’t happen: the await
conn.send_goodbye_msg()
call is still inside the cancelled block, so
it will also raise Cancelled
.
Of course, if you really want to make another blocking call in your
cleanup handler, trio will let you; it’s trying to prevent you from
accidentally shooting yourself in the foot. Intentional foot-shooting
is no problem (or at least – it’s not trio’s problem). To do this,
create a new scope, and set its shield
attribute to
True
:
with trio.move_on_after(TIMEOUT):
conn = make_connection()
try:
await conn.send_hello_msg()
finally:
with move_on_after(CLEANUP_TIMEOUT) as cleanup_scope:
cleanup_scope.shield = True
await conn.send_goodbye_msg()
So long as you’re inside a scope with shield = True
set, then
you’ll be protected from outside cancellations. Note though that this
only applies to outside cancellations: if CLEANUP_TIMEOUT
expires then await conn.send_goodbye_msg()
will still be
cancelled, and if await conn.send_goodbye_msg()
call uses any
timeouts internally, then those will continue to work normally as
well. This is a pretty advanced feature that most people probably
won’t use, but it’s there for the rare cases where you need it.
Cancellation and primitive operations¶
We’ve talked a lot about what happens when an operation is cancelled, and how you need to be prepared for this whenever calling a cancellable operation... but we haven’t gone into the details about which operations are cancellable, and how exactly they behave when they’re cancelled.
Here’s the rule: if it’s in the trio namespace, and you use await
to call it, then it’s cancellable (see Checkpoints
above). Cancellable means:
- If you try to call it when inside a cancelled scope, then it will
raise
Cancelled
. - If it blocks, and while it’s blocked then one of the scopes around
it becomes cancelled, it will return early and raise
Cancelled
. - Raising
Cancelled
means that the operation did not happen. If a trio socket’ssend
method raisesCancelled
, then no data was sent. If a trio socket’srecv
method raisesCancelled
then no data was lost – it’s still sitting in the socket recieve buffer waiting for you to callrecv
again. And so forth.
There are a few idiosyncratic cases where external constraints make it impossible to fully implement these semantics. These are always documented. There is also one systematic exception:
- Async cleanup operations – like
__aexit__
methods or async close methods – are cancellable just like anything else except that if they are cancelled, they still perform a minimum level of cleanup before raisingCancelled
.
For example, closing a TLS-wrapped socket normally involves sending a
notification to the remote peer, so that they can be cryptographically
assured that you really meant to close the socket, and your connection
wasn’t just broken by a man-in-the-middle attacker. But handling this
robustly is a bit tricky. Remember our example above where the blocking
send_goodbye_msg
caused problems? That’s exactly how closing a TLS
socket works: if the remote peer has disappeared, then our code may
never be able to actually send our shutdown notification, and it would
be nice if it didn’t block forever trying. Therefore, the method for
closing a TLS-wrapped socket will try to send that notification –
and if it gets cancelled, then it will give up on sending the message,
but will still close the underlying socket before raising
Cancelled
, so at least you don’t leak that resource.
Cancellation API details¶
The primitive operation for creating a new cancellation scope is:
-
with
trio.
open_cancel_scope
(*, deadline=inf, shield=False) as cancel_scope¶ Returns a context manager which creates a new cancellation scope.
Cancel scope objects provide the following interface:
-
deadline
¶ Read-write,
float
. An absolute time on the current run’s clock at which this scope will automatically become cancelled. You can adjust the deadline by modifying this attribute, e.g.:# I need a little more time! cancel_scope.deadline += 30
Note that for efficiency, the core run loop only checks for expired deadlines every once in a while. This means that in certain cases there may be a short delay between when the clock says the deadline should have expired, and when checkpoints start raising
Cancelled
. This is a very obscure corner case that you’re unlikely to notice, but we document it for completeness. (If this does cause problems for you, of course, then we want to know!)Defaults to
math.inf
, which means “no deadline”, though this can be overridden by thedeadline=
argument toopen_cancel_scope()
.
-
shield
¶ Read-write,
bool
, defaultFalse
. So long as this is set toTrue
, then the code inside this scope will not receiveCancelled
exceptions from scopes that are outside this scope. They can still receiveCancelled
exceptions from (1) this scope, or (2) scopes inside this scope. You can modify this attribute:with trio.open_cancel_scope() as cancel_scope: cancel_scope.shield = True # This cannot be interrupted by any means short of # killing the process: await sleep(10) cancel_scope.shield = False # Now this can be cancelled normally: await sleep(10)
Defaults to
False
, though this can be overridden by theshield=
argument toopen_cancel_scope()
.
-
cancel
()¶ Cancels this scope immediately.
This method is idempotent, i.e. if the scope was already cancelled then this method silently does nothing.
-
Trio also provides several convenience functions for the common situation of just wanting to impose a timeout on some code:
-
with
trio.
move_on_after
(seconds) as cancel_scope¶ Use as a context manager to create a cancel scope whose deadline is set to now + seconds.
Parameters: seconds (float) – The timeout. Raises: ValueError
– if timeout is less than zero.
-
with
trio.
move_on_at
(deadline) as cancel_scope¶ Use as a context manager to create a cancel scope with the given absolute deadline.
Parameters: deadline (float) – The deadline.
-
with
trio.
fail_after
(seconds) as cancel_scope¶ Creates a cancel scope with the given timeout, and raises an error if it is actually cancelled.
This function and
move_on_after()
are similar in that both create a cancel scope with a given timeout, and if the timeout expires then both will causeCancelled
to be raised within the scope. The difference is that when theCancelled
exception reachesmove_on_after()
, it’s caught and discarded. When it reachesfail_after()
, then it’s caught andTooSlowError
is raised in its place.Raises: TooSlowError
– if aCancelled
exception is raised in this scope and caught by the context manager.ValueError
– if seconds is less than zero.
-
with
trio.
fail_at
(deadline) as cancel_scope¶ Creates a cancel scope with the given deadline, and raises an error if it is actually cancelled.
This function and
move_on_at()
are similar in that both create a cancel scope with a given absolute deadline, and if the deadline expires then both will causeCancelled
to be raised within the scope. The difference is that when theCancelled
exception reachesmove_on_at()
, it’s caught and discarded. When it reachesfail_after()
, then it’s caught andTooSlowError
is raised in its place.Raises: TooSlowError
– if aCancelled
exception is raised in this scope and caught by the context manager.
Cheat sheet:
If you want to impose a timeout on a function, but you don’t care whether it timed out or not:
with trio.move_on_after(TIMEOUT): await do_whatever() # carry on!
If you want to impose a timeout on a function, and then do some recovery if it timed out:
with trio.move_on_after(TIMEOUT) as cancel_scope: await do_whatever() if cancel_scope.cancelled_caught: # The operation timed out, try something else try_to_recover()
If you want to impose a timeout on a function, and then if it times out then just give up and raise an error for your caller to deal with:
with trio.fail_after(TIMEOUT): await do_whatever()
It’s also possible to check what the current effective deadline is, which is sometimes useful:
-
trio.
current_effective_deadline
()¶ Returns the current effective deadline for the current task.
This function examines all the cancellation scopes that are currently in effect (taking into account shielding), and returns the deadline that will expire first.
One example of where this might be is useful is if your code is trying to decide whether to begin an expensive operation like an RPC call, but wants to skip it if it knows that it can’t possibly complete in the available time. Another example would be if you’re using a protocol like gRPC that propagates timeout information to the remote peer; this function gives a way to fetch that information so you can send it along.
If this is called in a context where a cancellation is currently active (i.e., a blocking call will immediately raise
Cancelled
), then returned deadline is-inf
. If it is called in a context where no scopes have a deadline set, it returnsinf
.Returns: the effective deadline, as an absolute time. Return type: float
Tasks let you do multiple things at once¶
One of trio’s core design principles is: no implicit concurrency. Every function executes in a straightforward, top-to-bottom manner, finishing each operation before moving on to the next – like Guido intended.
But, of course, the entire point of an async library is to let you do multiple things at once. The one and only way to do that in trio is through the task spawning interface. So if you want your program to walk and chew gum, this is the section for you.
Nurseries and spawning¶
Most libraries for concurrent programming let you spawn new child tasks (or threads, or whatever) willy-nilly, whenever and where-ever you feel like it. Trio is a bit different: you can’t spawn a child task unless you’re prepared to be a responsible parent. The way you demonstrate your responsibility is by creating a nursery:
async with trio.open_nursery() as nursery:
...
And once you have a reference to a nursery object, you can spawn children into that nursery:
async def child():
...
async def parent():
async with trio.open_nursery() as nursery:
# Make two concurrent calls to child()
nursery.spawn(child)
nursery.spawn(child)
This means that tasks form a tree: when you call run()
, then
this creates an initial task, and all your other tasks will be
children, grandchildren, etc. of the initial task.
The crucial thing about this setup is that when execution reaches the
end of the async with
block, then the nursery cleanup code
runs. The nursery cleanup code does the following things:
- If the body of the
async with
block raised an exception, then it cancels all remaining child tasks and saves the exception. - It watches for child tasks to exit. If a child task exits with an exception, then it cancels all remaining child tasks and saves the exception.
- Once all child tasks have exited:
- It marks the nursery as “closed”, so no new tasks can be spawned in it.
- If there’s just one saved exception, it re-raises it, or
- If there are multiple saved exceptions, it re-raises them as a
MultiError
, or - if there are no saved exceptions, it exits normally.
Since all tasks are descendents of the initial task, one consequence
of this is that run()
can’t finish until all tasks have
finished.
Getting results from child tasks¶
The spawn
method returns a Task
object that can be used
for various things – and in particular, for retrieving the task’s
return value. Example:
async def child_fn(x):
return 2 * x
async with trio.open_nursery() as nursery:
child_task = nursery.spawn(child_fn, 3)
# We've left the nursery, so we know child_task has completed
assert child_task.result.unwrap() == 6
See Task.result
and Result
for more details.
Child tasks and cancellation¶
In trio, child tasks inherit the parent nursery’s cancel scopes. So in this example, both the child tasks will be cancelled when the timeout expires:
with move_on_after(TIMEOUT):
async with trio.open_nursery() as nursery:
nursery.spawn(child1)
nursery.spawn(child2)
Note that what matters here is the scopes that were active when
open_nursery()
was called, not the scopes active when
spawn
is called. So for example, the timeout block below does
nothing at all:
async with trio.open_nursery() as nursery:
with move_on_after(TIMEOUT): # don't do this!
nursery.spawn(child)
Errors in multiple child tasks¶
Normally, in Python, only one thing happens at a time, which means that only one thing can wrong at a time. Trio has no such limitation. Consider code like:
async def broken1():
d = {}
return d["missing"]
async def broken2():
seq = range(10)
return seq[20]
async def parent():
async with trio.open_nursery() as nursery:
nursery.spawn(broken1)
nursery.spawn(broken2)
broken1
raises KeyError
. broken2
raises
IndexError
. Obviously parent
should raise some error, but
what? In some sense, the answer should be “both of these at once”, but
in Python there can only be one exception at a time.
Trio’s answer is that it raises a MultiError
object. This is a
special exception which encapsulates multiple exception objects –
either regular exceptions or nested MultiError
s. To make these
easier to work with, trio installs a custom sys.excepthook
that
knows how to print nice tracebacks for unhandled MultiError
s,
and it also provides some helpful utilities like
MultiError.catch()
, which allows you to catch “part of” a
MultiError
.
How to be a good parent task¶
Supervising child tasks is a full time job. If you want your program to do two things at once, then don’t expect the parent task to do one while a child task does another – instead, spawn two children and let the parent focus on managing them.
So, don’t do this:
# bad idea!
async with trio.open_nursery() as nursery:
nursery.spawn(walk)
await chew_gum()
Instead, do this:
# good idea!
async with trio.open_nursery() as nursery:
nursery.spawn(walk)
nursery.spawn(chew_gum)
# now parent task blocks in the nursery cleanup code
The difference between these is that in the first example, if walk
crashes, the parent is off distracted chewing gum, and won’t
notice. In the second example, the parent is watching both children,
and will notice and respond appropriately if anything happens.
Spawning tasks without becoming a parent¶
Sometimes it doesn’t make sense for the task that spawns a child to take on responsibility for watching it. For example, a server task may want to spawn a new task for each connection, but it can’t listen for connections and supervise children at the same time.
The solution here is simple once you see it: there’s no requirement that a nursery object stay in the task that created it! We can write code like this:
async def new_connection_listener(handler, nursery):
while True:
conn = await get_new_connection()
nursery.spawn(handler, conn)
async def server(handler):
async with trio.open_nursery() as nursery:
nursery.spawn(new_connection_listener, handler, nursery)
Now new_connection_listener
can focus on handling new connections,
while its parent focuses on supervising both it and all the individual
connection handlers.
And remember that cancel scopes are inherited from the nursery,
not from the task that calls spawn
. So in this example, the
timeout does not apply to child
(or to anything else):
async with do_spawn(nursery):
with move_on_after(TIMEOUT): # don't do this, it has no effect
nursery.spawn(child)
async with trio.open_nursery() as nursery:
nursery.spawn(do_spawn, nursery)
Custom supervisors¶
The default cleanup logic is often sufficient for simple cases, but what if you want a more sophisticated supervisor? For example, maybe you have Erlang envy and want features like automatic restart of crashed tasks. Trio itself doesn’t provide such a feature, but the nursery interface is designed to give you all the tools you need to build such a thing, while enforcing basic hygiene (e.g., it’s not possible to build a supervisor that exits and leaves orphaned tasks behind). And then hopefully you’ll wrap your fancy supervisor up in a library and put it on PyPI, because building custom supervisors is a challenging task that most people don’t want to deal with!
For simple custom supervisors, it’s often possible to lean on the default nursery logic to take care of annoying details. For example, here’s a function that takes a list of functions, runs them all concurrently, and returns the result from the one that finishes first:
async def race(*async_fns):
if not async_fns:
raise ValueError("must pass at least one argument")
async with trio.open_nursery() as nursery:
for async_fn in async_fns:
nursery.spawn(async_fn)
task_batch = await nursery.monitor.get_batch()
nursery.cancel_scope.cancel()
finished_task = task_batch[0]
return nursery.reap_and_unwrap(finished_task)
This works by waiting until at least one task has finished, then cancelling all remaining tasks and returning the result from the first task. This implicitly invokes the default logic to take care of all the other tasks, so it blocks to wait for the cancellation to finish, and if any of them raise errors in the process it will propagate those.
Task-local storage and run-local storage¶
Synchronizing and communicating between tasks¶
Trio provides a standard set of synchronization and inter-task communication primitives. These objects’ APIs are generally modelled off of the analogous classes in the standard library, but with some differences.
Blocking and non-blocking methods¶
The standard library synchronization primitives have a variety of mechanisms for specifying timeouts and blocking behavior, and of signaling whether an operation returned due to success versus a timeout.
In trio, we standardize on the following conventions:
- We don’t provide timeout arguments. If you want a timeout, then use a cancel scope.
- For operations that have a non-blocking variant, the blocking and
non-blocking variants are different methods with names like
X
andX_nowait
, respectively. (This is similar toqueue.Queue
, but unlike most of the classes inthreading
.) We like this approach because it allows us to make the blocking version async and the non-blocking version sync. - When a non-blocking method cannot succeed (the queue is empty, the
lock is already held, etc.), then it raises
trio.WouldBlock
. There’s no equivalent to thequeue.Empty
versusqueue.Full
distinction – we just have the one exception that we use consistently.
Fairness¶
These classes are all guaranteed to be “fair”, meaning that when it comes time to choose who will be next to acquire a lock, get an item from a queue, etc., then it always goes to the task which has been waiting longest. It’s not entirely clear whether this is the best choice, but for now that’s how it works.
As an example of what this means, here’s a small program in which two
tasks compete for a lock. Notice that the task which releases the lock
always immedately attempts to re-acquire it, before the other task has
a chance to run. (And remember that we’re doing cooperative
multi-tasking here, so it’s actually deterministic that the task
releasing the lock will call acquire()
before the other
task wakes up; in trio releasing a lock is not a checkpoint.) With
an unfair lock, this would result in the same task holding the lock
forever and the other task being starved out. But if you run this,
you’ll see that the two tasks politely take turns:
# fairness-demo.py
import trio
async def loopy_child(number, lock):
while True:
async with lock:
print("Child {} has the lock!".format(number))
await trio.sleep(0.5)
async def main():
async with trio.open_nursery() as nursery:
lock = trio.Lock()
nursery.spawn(loopy_child, 1, lock)
nursery.spawn(loopy_child, 2, lock)
trio.run(main)
Broadcasting an event with Event
¶
-
class
trio.
Event
¶ A waitable boolean value useful for inter-task synchronization, inspired by
threading.Event
.An event object manages an internal boolean flag, which is initially False.
-
is_set
()¶ Return the current value of the internal flag.
-
set
()¶ Set the internal flag value to True, and wake any waiting tasks.
-
clear
()¶ Set the internal flag value to False.
-
await
wait
()¶ Block until the internal flag value becomes True.
If it’s already True, then this method is still a checkpoint, but otherwise returns immediately.
-
Passing messages with Queue
and UnboundedQueue
¶
Trio provides two types of queues suitable for different purposes. Where they differ is in their strategies for handling flow control. Here’s a toy example to demonstrate the problem. Suppose we have a queue with two producers and one consumer:
async def producer(queue):
while True:
await queue.put(1)
async def consumer(queue):
while True:
print(await queue.get())
async def main():
# Trio's actual queue classes have countermeasures to prevent
# this example from working, so imagine we have some sort of
# platonic ideal of a queue here
queue = trio.HypotheticalQueue()
async with trio.open_nursery() as nursery:
# Two producers
nursery.spawn(producer, queue)
nursery.spawn(producer, queue)
# One consumer
nursery.spawn(consumer, queue)
trio.run(main)
If we naively cycle between these three tasks in round-robin style, then we put an item, then put an item, then get an item, then put an item, then put an item, then get an item, ... and since on each cycle we add two items to the queue but only remove one, then over time the queue size grows arbitrarily large, our latency is terrible, we run out of memory, it’s just generally bad news all around.
There are two potential strategies for avoiding this problem.
The preferred solution is to apply backpressure. If our queue starts
getting too big, then we can make the producers slow down by having
put
block until get
has had a chance to remove an item. This
is the strategy used by trio.Queue
.
The other possibility is for the queue consumer to get greedy: each
time it runs, it could eagerly consume all of the pending items before
allowing another task to run. (In some other systems, this would
happen automatically because their queue’s get
method doesn’t
invoke the scheduler unless it has to block. But in trio, get is
always a checkpoint.) This would work, but it’s a
bit risky: basically instead of applying backpressure to specifically
the producer tasks, we’re applying it to all the tasks in our
system. The danger here is that if enough items have built up in the
queue, then “stopping the world” to process them all may cause
unacceptable latency spikes in unrelated tasks. Nonetheless, this is
still the right choice in situations where it’s impossible to apply
backpressure more precisely. For example, when monitoring exiting
tasks, blocking tasks from reporting their death doesn’t really
accomplish anything – the tasks are taking up memory either way,
etc. (In this particular case it might be possible to do better, but in general the
principle holds.) So this is the strategy implemented by
trio.UnboundedQueue
.
tl;dr: use Queue
if you can.
-
class
trio.
Queue
(capacity)¶ A bounded queue suitable for inter-task communication.
This class is generally modelled after
queue.Queue
, but with the major difference that it is always bounded. For an unbounded queue, seetrio.UnboundedQueue
.A
Queue
object can be used as an asynchronous iterator, that dequeues objects one at a time. I.e., these two loops are equivalent:async for obj in queue: ... while True: obj = await queue.get() ...
Parameters: capacity (int) – The maximum number of items allowed in the queue before put()
blocks. Choosing a sensible value here is important to ensure that backpressure is communicated promptly and avoid unnecessary latency. If in doubt, use 1.-
qsize
()¶ Returns the number of items currently in the queue.
There is some subtlety to interpreting this method’s return value: see issue #63.
-
full
()¶ Returns True if the queue is at capacity, False otherwise.
There is some subtlety to interpreting this method’s return value: see issue #63.
-
empty
()¶ Returns True if the queue is empty, False otherwise.
There is some subtlety to interpreting this method’s return value: see issue #63.
-
put_nowait
(obj)¶ Attempt to put an object into the queue, without blocking.
Parameters: obj (object) – The object to enqueue. Raises: WouldBlock
– if the queue is full.
-
await
put
(obj)¶ Put an object into the queue, blocking if necessary.
Parameters: obj (object) – The object to enqueue.
-
await
get_nowait
()¶ Attempt to get an object from the queue, without blocking.
Returns: The dequeued object. Return type: object Raises: WouldBlock
– if the queue is empty.
-
await
get
()¶ Get an object from the queue, blocking is necessary.
Returns: The dequeued object. Return type: object
-
await
task_done
()¶ Decrement the count of unfinished work.
Each
Queue
object keeps a count of unfinished work, which starts at zero and is incremented after each successfulput()
. This method decrements it again. When the count reaches zero, any tasks blocked injoin()
are woken.
-
await
join
()¶ Block until the count of unfinished work reaches zero.
See
task_done()
for details.
-
await
statistics
()¶ Returns an object containing debugging information.
Currently the following fields are defined:
qsize
: The number of items currently in the queue.capacity
: The maximum number of items the queue can hold.tasks_waiting_put
: The number of tasks blocked on this queue’sput()
method.tasks_waiting_get
: The number of tasks blocked on this queue’sget()
method.tasks_waiting_join
: The number of tasks blocked on this queue’sjoin()
method.
-
-
class
trio.
UnboundedQueue
¶ An unbounded queue suitable for certain unusual forms of inter-task communication.
This class is designed for use as a queue in cases where the producer for some reason cannot be subjected to back-pressure, i.e.,
put_nowait()
has to always succeed. In order to prevent the queue backlog from actually growing without bound, the consumer API is modified to dequeue items in “batches”. If a consumer task processes each batch without yielding, then this helps achieve (but does not guarantee) an effective bound on the queue’s memory use, at the cost of potentially increasing system latencies in general. You should generally prefer to use aQueue
instead if you can.Currently each batch completely empties the queue, but this may change in the future.
A
UnboundedQueue
object can be used as an asynchronous iterator, where each iteration returns a new batch of items. I.e., these two loops are equivalent:async for batch in queue: ... while True: obj = await queue.get_batch() ...
-
qsize
()¶ Returns the number of items currently in the queue.
-
empty
()¶ Returns True if the queue is empty, False otherwise.
There is some subtlety to interpreting this method’s return value: see issue #63.
-
put_nowait
(obj)¶ Put an object into the queue, without blocking.
This always succeeds, because the queue is unbounded. We don’t provide a blocking
put
method, because it would never need to block.Parameters: obj (object) – The object to enqueue.
-
get_batch_nowait
()¶ Attempt to get the next batch from the queue, without blocking.
Returns: - A list of dequeued items, in order. On a successful call this
- list is always non-empty; if it would be empty we raise
WouldBlock
instead.
Return type: list Raises: WouldBlock
– if the queue is empty.
-
await
get_batch
()¶ Get the next batch from the queue, blocking as necessary.
Returns: - A list of dequeued items, in order. This list is always
- non-empty.
Return type: list
-
await
statistics
()¶ Return an object containing debugging information.
Currently the following fields are defined:
qsize
: The number of items currently in the queue.tasks_waiting
: The number of tasks blocked on this queue’sget_batch()
method.
-
Lower-level synchronization primitives¶
Personally, I find that events and queues are usually enough to
implement most things I care about, and lead to easier to read code
than the lower-level primitives discussed in this section. But if you
need them, they’re here. (If you find yourself reaching for these
because you’re trying to implement a new higher-level synchronization
primitive, then you might also want to check out the facilities in
trio.hazmat
for a more direct exposure of trio’s underlying
synchronization logic. All of classes discussed in this section are
implemented on top of the public APIs in trio.hazmat
; they
don’t have any special access to trio’s internals.)
-
class
trio.
Semaphore
(initial_value, *, max_value=None)¶ A semaphore.
A semaphore holds an integer value, which can be incremented by calling
release()
and decremented by callingacquire()
– but the value is never allowed to drop below zero. If the value is zero, thenacquire()
will block until someone callsrelease()
.This is a very flexible synchronization object, but perhaps the most common use is to represent a resource with some bounded supply. For example, if you want to make sure that there are never more than four tasks simultaneously performing some operation, you could do something like:
# Allocate a shared Semaphore object, and somehow distribute it to all # your tasks. NB: max_value=4 isn't technically necessary, but can # help catch errors. sem = trio.Semaphore(4, max_value=4) # Then when you perform the operation: async with sem: await perform_operation()
This object’s interface is similar to, but different from, that of
threading.Semaphore
.A
Semaphore
object can be used as an async context manager; it blocks on entry but not on exit.Parameters: -
value
¶ The current value of the semaphore.
-
max_value
¶ The maximum allowed value. May be None to indicate no limit.
-
acquire_nowait
()¶ Attempt to decrement the semaphore value, without blocking.
Raises: WouldBlock
– if the value is zero.
-
await
acquire
()¶ Decrement the semaphore value, blocking if necessary to avoid letting it drop below zero.
-
await
release
()¶ Increment the semaphore value, possibly waking a task blocked in
acquire()
.Raises: ValueError
– if incrementing the value would cause it to exceedmax_value
.
-
-
class
trio.
Lock
¶ A classic mutex.
This is a non-reentrant, single-owner lock. Unlike
threading.Lock
, only the owner of the lock is allowed to release it.A
Lock
object can be used as an async context manager; it blocks on entry but not on exit.-
locked
()¶ Check whether the lock is currently held.
Returns: True if the lock is held, False otherwise. Return type: bool
-
acquire_nowait
()¶ Attempt to acquire the lock, without blocking.
Raises: WouldBlock
– if the lock is held.
-
await
acquire
()¶ Acquire the lock, blocking if necessary.
-
await
release
()¶ Release the lock.
Raises: RuntimeError
– if the calling task does not hold the lock.
-
await
statistics
()¶ Return an object containing debugging information.
Currently the following fields are defined:
-
-
class
trio.
Condition
(lock=None)¶ A classic condition variable, similar to
threading.Condition
.A
Condition
object can be used as an async context manager to acquire the underlying lock; it blocks on entry but not on exit.Parameters: lock (Lock) – the lock object to use. If given, must be a trio.Lock
. If None, a newLock
will be allocated and used.-
locked
()¶ Check whether the underlying lock is currently held.
Returns: True if the lock is held, False otherwise. Return type: bool
-
acquire_nowait
()¶ Attempt to acquire the underlying lock, without blocking.
Raises: WouldBlock
– if the lock is currently held.
-
await
acquire
()¶ Acquire the underlying lock, blocking if necessary.
-
await
release
()¶ Release the underlying lock.
-
await
wait
()¶ Wait for another thread to call
notify()
ornotify_all()
.When calling this method, you must hold the lock. It releases the lock while waiting, and then re-acquires it before waking up.
There is a subtlety with how this method interacts with cancellation: when cancelled it will block to re-acquire the lock before raising
Cancelled
. This may cause cancellation to be less prompt than expected. The advantage is that it makes code like this work:async with condition: await condition.wait()
If we didn’t re-acquire the lock before waking up, and
wait()
were cancelled here, then we’d crash incondition.__aexit__
when we tried to release the lock we no longer held.Raises: RuntimeError
– if the calling task does not hold the lock.
-
await
notify
(n=1)¶ Wake one or more tasks that are blocked in
wait()
.Parameters: n (int) – The number of tasks to wake. Raises: RuntimeError
– if the calling task does not hold the lock.
-
await
notify_all
()¶ Wake all tasks that are currently blocked in
wait()
.Raises: RuntimeError
– if the calling task does not hold the lock.
-
await
statistics
()¶ Return an object containing debugging information.
Currently the following fields are defined:
tasks_waiting
: The number of tasks blocked on this condition’swait()
method.lock_statistics
: The result of calling the underlyingLock
sstatistics()
method.
-
Threads (if you must)¶
In a perfect world, all third-party libraries and low-level APIs would be natively async and integrated into Trio, and all would be happiness and rainbows.
That world, alas, does not (yet) exist. Until it does, you may find yourself needing to interact with non-Trio APIs that do rude things like “blocking”.
In acknowledgment of this reality, Trio provides two useful utilities
for working with real, operating-system level,
threading
-module-style threads. First, if you’re in Trio but
need to push some work into a thread, there’s
run_in_worker_thread()
. And if you’re in a thread and need to
communicate back with trio, there’s the closely related
current_run_in_trio_thread()
and
current_await_in_trio_thread()
.
-
await
trio.
run_in_worker_thread
(sync_fn, *args, cancellable=False)¶ Convert a blocking operation in an async operation using a thread.
These two lines are equivalent:
sync_fn(*args) await run_in_worker_thread(sync_fn, *args)
except that if
sync_fn
takes a long time, then the first line will block the Trio loop while it runs, while the second line allows other Trio tasks to continue working whilesync_fn
runs. This is accomplished by pushing the call tosync_fn(*args)
off into a worker thread.Cancellation handling: Cancellation is a tricky issue here, because neither Python nor the operating systems it runs on provide any general way to communicate with an arbitrary synchronous function running in a thread and tell it to stop. This function will always check for cancellation on entry, before starting the thread. But once the thread is running, there are two ways it can handle being cancelled:
- If
cancellable=False
, the function ignores the cancellation and keeps going, just like if we had calledsync_fn
synchronously. This is the default behavior. - If
cancellable=True
, thenrun_in_worker_thread
immediately raisesCancelled
. In this case the thread keeps running in background – we just abandon it to do whatever it’s going to do, and silently discard any return value or errors that it raises. Only use this if you know that the operation is safe and side-effect free. (For example:trio.socket.getaddrinfo
is implemented usingrun_in_worker_thread()
, and it setscancellable=True
because it doesn’t really matter if a stray hostname lookup keeps running in the background.)
Warning
You should not use
run_in_worker_thread()
to call CPU-bound functions! In addition to the usual GIL-related reasons why using threads for CPU-bound work is not very effective in Python, there is an additional problem: on CPython, CPU-bound threads tend to “starve out” IO-bound threads, so usingrun_in_worker_thread()
for CPU-bound work is likely to adversely affect the main thread running trio. If you need to do this, you’re better off using a worker process, or perhaps PyPy (which still has a GIL, but may do a better job of fairly allocating CPU time between threads).Parameters: - sync_fn – An arbitrary synchronous callable.
- *args – Positional arguments to pass to sync_fn. If you need keyword
arguments, use
functools.partial()
. - cancellable (bool) – Whether to allow cancellation of this operation. See discussion above.
Returns: Whatever
sync_fn(*args)
returns.Raises: Whatever
sync_fn(*args)
raises.- If
-
trio.
current_run_in_trio_thread
()¶ -
trio.
current_await_in_trio_thread
()¶ Call these from inside a trio run to get a reference to the current run’s
run_in_trio_thread()
orawait_in_trio_thread()
:-
run_in_trio_thread
(sync_fn, *args)¶
-
await_in_trio_thread
(async_fn, *args)¶
These functions schedule a call to
sync_fn(*args)
orawait async_fn(*args)
to happen in the main trio thread, wait for it to complete, and then return the result or raise whatever exception it raised.These are the only non-hazmat functions that interact with the trio run loop and that can safely be called from a different thread than the one that called
trio.run()
. These two functions must be called from a different thread than the one that calledtrio.run()
. (After all, they’re blocking functions!)Warning
If the relevant call to
trio.run()
finishes while a call toawait_in_trio_thread
is in progress, then the call toasync_fn
will be cancelled and the resultingCancelled
exception may propagate out ofawait_in_trio_thread
and into the calling thread. You should be prepared for this.Raises: RunFinishedError – If the corresponding call to trio.run()
has already completed.-
This will probably be clearer with an example. Here we demonstrate how
to spawn a child thread, and then use a trio.Queue
to send
messages between the thread and a trio task:
import trio
import threading
def thread_fn(await_in_trio_thread, request_queue, response_queue):
while True:
# Since we're in a thread, we can't call trio.Queue methods
# directly -- so we use await_in_trio_thread to call them.
request = await_in_trio_thread(request_queue.get)
# We use 'None' as a request to quit
if request is not None:
response = request + 1
await_in_trio_thread(response_queue.put, response)
else:
# acknowledge that we're shutting down, and then do it
await_in_trio_thread(response_queue.put, None)
return
async def main():
# Get a reference to the await_in_trio_thread function
await_in_trio_thread = trio.current_await_in_trio_thread()
request_queue = trio.Queue(1)
response_queue = trio.Queue(1)
thread = threading.Thread(
target=thread_fn,
args=(await_in_trio_thread, request_queue, response_queue))
thread.start()
# prints "1"
await request_queue.put(0)
print(await response_queue.get())
# prints "2"
await request_queue.put(1)
print(await response_queue.get())
# prints "None"
await request_queue.put(None)
print(await response_queue.get())
thread.join()
trio.run(main)
Debugging and instrumentation¶
Trio tries hard to provide useful hooks for debugging and
instrumentation. Some are documented above (Task.name
,
Queue.statistics()
, etc.). Here are some more:
Global statistics¶
-
trio.
current_statistics
()¶ Returns an object containing run-loop-level debugging information.
Currently the following fields are defined:
tasks_living
(int): The number of tasks that have been spawned and not yet exited.tasks_runnable
(int): The number of tasks that are currently queued on the run queue (as opposed to blocked waiting for something to happen).seconds_to_next_deadline
(float): The time until the next pending cancel scope deadline. May be negative if the deadline has expired but we haven’t yet processed cancellations. May beinf
if there are no pending deadlines.call_soon_queue_size
(int): The number of unprocessed callbacks queued viatrio.hazmat.current_call_soon_thread_and_signal_safe()
.io_statistics
(object): Some statistics from trio’s I/O backend. This always has an attributebackend
which is a string naming which operating-system-specific I/O backend is in use; the other attributes vary between backends.
Instrument API¶
The instrument API provides a standard way to add custom instrumentation to the run loop. Want to make a histogram of scheduling latencies, log a stack trace of any task that blocks the run loop for >50 ms, or measure what percentage of your process’s running time is spent waiting for I/O? This is the place.
The general idea is that at any given moment, trio.run()
maintains a set of “instruments”, which are objects that implement the
trio.abc.Instrument
interface. When an interesting event
happens, it loops over these instruments and notifies them by calling
an appropriate method. The tutorial has a simple example of
using this for tracing.
Since this hooks into trio at a rather low level, you do have to be somewhat careful. The callbacks are run synchronously, and in many cases if they error out then there isn’t any plausible way to propagate this exception (for instance, we might be deep in the guts of the exception propagation machinery...). Therefore our current strategy for handling exceptions raised by instruments is to (a) dump a stack trace to stderr and (b) disable the offending instrument.
You can register an initial list of instruments by passing them to
trio.run()
. current_instruments()
lets you introspect and
modify this list at runtime from inside trio:
-
trio.
current_instruments
()¶ Returns the list of currently active instruments.
This list is live: if you mutate it, then
trio.run()
will stop calling the instruments you remove and start calling the ones you add.
And here’s the instrument API:
-
class
trio.abc.
Instrument
¶ The interface for run loop instrumentation.
Instruments don’t have to inherit from this abstract base class, and all of these methods are optional. This class serves mostly as documentation.
-
before_run
()¶ Called at the beginning of
trio.run()
.
-
after_run
()¶ Called just before
trio.run()
returns.
-
task_spawned
(task)¶ Called when the given task is created.
Parameters: task (trio.Task) – The new task.
-
task_scheduled
(task)¶ Called when the given task becomes runnable.
It may still be some time before it actually runs, if there are other runnable tasks ahead of it.
Parameters: task (trio.Task) – The task that became runnable.
-
before_task_step
(task)¶ Called immediately before we resume running the given task.
Parameters: task (trio.Task) – The task that is about to run.
-
after_task_step
(task)¶ Called when we return to the main run loop after a task has yielded.
Parameters: task (trio.Task) – The task that just ran.
-
task_exited
(task)¶ Called when the given task exits.
Parameters: task (trio.Task) – The finished task.
-
The tutorial has a fully-worked example of defining a custom instrument to log trio’s internal scheduling decisions.
Exceptions¶
-
exception
trio.
TrioInternalError
¶ Raised by
run()
if we encounter a bug in trio, or (possibly) a misuse of one of the low-leveltrio.hazmat
APIs.This should never happen! If you get this error, please file a bug.
Unfortunately, if you get this error it also means that all bets are off – trio doesn’t know what is going on and its normal invariants may be void. (For example, we might have “lost track” of a task. Or lost track of all tasks.) Again, though, this shouldn’t happen.
-
exception
trio.
Cancelled
¶ Raised by blocking calls if the surrounding scope has been cancelled.
You should let this exception propagate, to be caught by the relevant cancel scope. To remind you of this, it inherits from
BaseException
instead ofException
, just likeKeyboardInterrupt
andSystemExit
do. This means that if you write something like:try: ... except Exception: ...
then this won’t catch a
Cancelled
exception.Note
In the US it’s also common to see this word spelled “canceled”, with only one “l”. This is a recent and US-specific innovation, and even in the US both forms are still commonly used. So for consistency with the rest of the world and with “cancellation” (which always has two “l”s), trio uses the two “l” spelling everywhere.
-
exception
trio.
TooSlowError
¶ Raised by
fail_after()
andfail_at()
if the timeout expires.
-
exception
trio.
WouldBlock
¶ Raised by
X_nowait
functions ifX
would block.
-
exception
trio.
RunFinishedError
¶ Raised by
run_in_trio_thread
and similar functions if the corresponding call totrio.run()
has already finished.
I/O in Trio¶
Sockets and networking¶
The trio.socket
module provides trio’s basic networking API.
trio.socket
‘s top-level exports¶
Generally, trio.socket
‘s API mirrors that of the standard
library socket
module. Most constants (like SOL_SOCKET
) and
simple utilities (like inet_aton()
) are simply
re-exported unchanged. But there are also some differences:
All functions that return sockets (e.g. socket.socket()
,
socket.socketpair()
, ...) are modified to return trio sockets
instead. In addition, there is a new function to directly convert a
standard library socket into a trio socket:
-
trio.socket.
from_stdlib_socket
(sock)¶ Convert a standard library
socket.socket()
into a trio socket.
The following functions have identical interfaces to their standard
library version, but are now async
functions, so you need to use
await
to call them:
Trio intentionally DOES NOT include some obsolete, redundant, or broken features:
gethostbyname()
,gethostbyname_ex()
,gethostbyaddr()
: obsolete; usegetaddrinfo()
andgetnameinfo()
instead.getdefaulttimeout()
,setdefaulttimeout()
: Use trio’s standard support for Cancellation and timeouts.- On Windows,
SO_REUSEADDR
is not exported, because it’s a trap: the name is the same as UnixSO_REUSEADDR
, but the semantics are different and extremely broken. In the very rare cases where you actually wantSO_REUSEADDR
on Windows, then it can still be accessed from the standard library’ssocket
module.
Socket objects¶
-
class
trio.socket.
SocketType
¶ Trio socket objects are overall very similar to the standard library socket objects, with a few important differences:
Async all the things: Most obviously, everything is made “trio-style”: blocking methods become async methods, and the following attributes are not supported:
setblocking()
: trio sockets always act like blocking sockets; if you need to read/write from multiple sockets at once, then create multiple tasks.settimeout()
: see Cancellation and timeouts instead.makefile()
: Python’s file-like API is synchronous, so it can’t be implemented on top of an async socket.
No implicit name resolution: In the standard library
socket
API, there are number of methods that take network addresses as arguments. When given a numeric address this is fine:# OK sock.bind(("127.0.0.1", 80)) sock.connect(("2607:f8b0:4000:80f::200e", 80))
But in the standard library, these methods also accept hostnames, and in this case implicitly trigger a DNS lookup to find the IP address:
# Might block! sock.bind(("localhost", 80)) sock.connect(("google.com", 80))
This is problematic because DNS lookups are a blocking operation.
For simplicity, trio forbids such usages: hostnames must be “pre-resolved” to numeric addresses before they are passed to socket methods like
bind()
orconnect()
. In most cases this can be easily accomplished by calling eitherresolve_local_address()
orresolve_remote_address()
.-
await
resolve_local_address
(address)¶ Resolve the given address into a numeric address suitable for passing to
bind()
.This performs the same address resolution that the standard library
bind()
call would do, taking into account the current socket’s settings (e.g. if this is an IPv6 socket then it returns IPv6 addresses). In particular, a hostname ofNone
is mapped to the wildcard address.
-
await
resolve_remote_address
(address)¶ Resolve the given address into a numeric address suitable for passing to
connect()
or similar.This performs the same address resolution that the standard library
connect()
call would do, taking into account the current socket’s settings (e.g. if this is an IPv6 socket then it returns IPv6 addresses). In particular, a hostname ofNone
is mapped to the localhost address.
Modern defaults: And finally, we took the opportunity to update the defaults for several socket options that were stuck in the 1980s. You can always use
setsockopt()
to change these back, but for trio sockets:Everywhere except Windows,
SO_REUSEADDR
is enabled by default. This is almost always what you want, but if you’re in one of the rare cases where this is undesireable then you can always disableSO_REUSEADDR
manually:sock.setsockopt(trio.socket.SOL_SOCKET, trio.socket.SO_REUSEADDR, False)
On Windows,
SO_EXCLUSIVEADDR
is enabled by default. Unfortunately, this means that if you stop and restart a server you may have trouble reacquiring listen ports (i.e., it acts like Unix withoutSO_REUSEADDR
). To get the Unix-styleSO_REUSEADDR
semantics on Windows, you can disableSO_EXCLUSIVEADDR
:sock.setsockopt(trio.socket.SOL_SOCKET, trio.socket.SO_EXCLUSIVEADDR, False)
but be warned that this may leave your application vulnerable to port hijacking attacks.
TCP_NODELAY
is enabled by default.IPV6_V6ONLY
is disabled, i.e., by default on dual-stack hosts aAF_INET6
socket is able to communicate with both IPv4 and IPv6 peers, where the IPv4 peers appear to be in the “IPv4-mapped” portion of IPv6 address space. To make an IPv6-only socket, use something like:sock = trio.socket.socket(trio.socket.AF_INET6) sock.setsockopt(trio.socket.IPPROTO_IPV6, trio.socket.IPV6_V6ONLY, True)
This makes trio applications behave more consistently across different environments.
On platforms where it’s supported (recent Linux and recent MacOS),
TCP_NOTSENT_LOWAT
is enabled with a reasonable buffer size (currently 16 KiB).
See issue #72 for discussion of these defaults.
The following methods are similar, but not identical, to the equivalents in
socket.socket()
:-
bind
(address)¶ Bind this socket to the given address.
Unlike the stdlib
connect()
, this method requires a pre-resolved address. Seeresolve_local_address()
.
-
await
connect
(address)¶ Connect the socket to a remote address.
Similar to
socket.socket.connect()
, except async and requiring a pre-resolved address. Seeresolve_remote_address()
.Warning
Due to limitations of the underlying operating system APIs, it is not always possible to properly cancel a connection attempt once it has begun. If
connect()
is cancelled, and is unable to abort the connection attempt, then it will:- forcibly close the socket to prevent accidental re-use
- raise
Cancelled
.
tl;dr: if
connect()
is cancelled then you should throw away that socket and make a new one.
-
await
sendall
(data, flags=0)¶ Send the data to the socket, blocking until all of it has been accepted by the operating system.
flags
are passed on tosend
.If an error occurs or the operation is cancelled, then the resulting exception will have a
.partial_result
attribute with a.bytes_sent
attribute containing the number of bytes sent.
-
sendfile
()¶
The following methods are not provided:
send()
: This method has confusing semantics hidden under a friendly name, and makes it too easy to create subtle bugs. Usesendall()
instead.
The following methods are identical to their equivalents in
socket.socket()
, except async, and the ones that take address arguments require pre-resolved addresses:accept()
recv()
recv_into()
recvfrom()
recvfrom_into()
recvmsg()
(if available)recvmsg_into()
(if available)sendto()
sendmsg()
(if available)
All methods and attributes not mentioned above are identical to their equivalents in
socket.socket()
:
The abstract Stream API¶
(this is currently more of a sketch than something actually useful, see issue #73)
-
class
trio.
AsyncResource
¶ -
abstractmethod
forceful_close
()¶ Force an immediate close of this resource.
This will never block, but (depending on the resource in question) it might be a “rude” shutdown.
-
abstractmethod await
graceful_close
()¶ Close this resource, gracefully.
This may block in order to perform a “graceful” shutdown (for example, sending a message alerting the other side of a connection that it is about to close). But, if cancelled, then it still must close the underlying resource.
Default implementation is to perform a
forceful_close()
and then execute a checkpoint.
-
abstractmethod
TLS support¶
Async disk I/O¶
Subprocesses¶
Signals¶
-
with
trio.
catch_signals
(signals) as batched_signal_aiter¶ A context manager for catching signals.
Entering this context manager starts listening for the given signals and returns an async iterator; exiting the context manager stops listening.
The async iterator blocks until at least one signal has arrived, and then yields a
set
containing all of the signals that were received since the last iteration. (This is generally similar to howUnboundedQueue
works, but since Unix semantics are that identical signals can/should be coalesced, here we use aset
for storage instead of alist
.)Note that if you leave the
with
block while the iterator has unextracted signals still pending inside it, then they will be re-delivered using Python’s regular signal handling logic. This avoids a race condition when signals arrives just before we exit thewith
block.Parameters: signals – a set of signals to listen for. Raises: RuntimeError
– if you try to use this anywhere except Python’s main thread. (This is a Python limitation.)Example
A common convention for Unix daemon is that they should reload their configuration when they receive a
SIGHUP
. Here’s a sketch of what that might look like usingcatch_signals()
:with trio.catch_signals({signal.SIGHUP}) as batched_signal_aiter: async for batch in batched_signal_aiter: # We're only listening for one signal, so the batch is always # {signal.SIGHUP}, but if we were listening to more signals # then it could vary. for signum in batch: assert signum == signal.SIGHUP reload_configuration()
Testing made easier with trio.testing
¶
The trio.testing
module provides various utilities to make it
easier to test trio code. Unlike the other submodules in the
trio
namespace, trio.testing
is not automatically
imported when you do import trio
; you must import trio.testing
explicitly.
Time and timeouts¶
trio.testing.MockClock
is a Clock
with a
few tricks up its sleeve to help you efficiently test code involving
timeouts:
- By default, it starts at time 0, and clock time only advances when
you explicitly call
jump()
. This provides an extremely controllable clock for testing. - You can set
rate
to 1.0 if you want it to start running in real time like a regular clock. You can stop and start the clock within a test. You can setrate
to 10.0 to make clock time pass at 10x real speed (so e.g.await trio.sleep(10)
returns after 1 second). - But even more interestingly, you can set
autojump_threshold
to zero or a small value, and then it will watch the execution of the run loop, and any time things have settled down and everyone’s waiting for a timeout, it jumps the clock forward to that timeout. In many cases this allows natural-looking code involving timeouts to be automatically run at near full CPU utilization with no changes. (Thanks to fluxcapacitor for this awesome idea.) - And of course these can be mixed and matched at will.
Regardless of these shenanigans, from “inside” trio the passage of time still seems normal so long as you restrict yourself to trio’s time functions (see Time and clocks). Below is an example demonstrating two different ways of making time pass quickly. Notice how in both cases, the two tasks keep a consistent view of reality and events happen in the expected order, despite being wildly divorced from real time:
# across-realtime.py
import time
import trio
import trio.testing
YEAR = 365 * 24 * 60 * 60 # seconds
async def task1():
start = trio.current_time()
print("task1: sleeping for 1 year")
await trio.sleep(YEAR)
duration = trio.current_time() - start
print("task1: woke up; clock says I've slept {} years"
.format(duration / YEAR))
print("task1: sleeping for 1 year, 100 times")
for _ in range(100):
await trio.sleep(YEAR)
duration = trio.current_time() - start
print("task1: slept {} years total".format(duration / YEAR))
async def task2():
start = trio.current_time()
print("task2: sleeping for 5 years")
await trio.sleep(5 * YEAR)
duration = trio.current_time() - start
print("task2: woke up; clock says I've slept {} years"
.format(duration / YEAR))
print("task2: sleeping for 500 years")
await trio.sleep(500 * YEAR)
duration = trio.current_time() - start
print("task2: slept {} years total".format(duration / YEAR))
async def main():
async with trio.open_nursery() as nursery:
nursery.spawn(task1)
nursery.spawn(task2)
def run_example(clock):
real_start = time.time()
trio.run(main, clock=clock)
real_duration = time.time() - real_start
print("Total real time elapsed: {} seconds".format(real_duration))
print("Clock where time passes at 100 years per second:\n")
run_example(trio.testing.MockClock(rate=100 * YEAR))
print("\nClock where time automatically skips past the boring parts:\n")
run_example(trio.testing.MockClock(autojump_threshold=0))
Output:
Clock where time passes at 100 years per second:
task2: sleeping for 5 years
task1: sleeping for 1 year
task1: woke up; clock says I've slept 1.0365006048232317 years
task1: sleeping for 1 year, 100 times
task2: woke up; clock says I've slept 5.0572111969813704 years
task2: sleeping for 500 years
task1: slept 104.77677842136472 years total
task2: slept 505.25014589075 years total
Total real time elapsed: 5.053582429885864 seconds
Clock where time automatically skips past the boring parts:
task2: sleeping for 5 years
task1: sleeping for 1 year
task1: woke up; clock says I've slept 1.0 years
task1: sleeping for 1 year, 100 times
task2: woke up; clock says I've slept 5.0 years
task2: sleeping for 500 years
task1: slept 101.0 years total
task2: slept 505.0 years total
Total real time elapsed: 0.019298791885375977 seconds
-
class
trio.testing.
MockClock
(rate=0.0, autojump_threshold=inf)¶ A user-controllable clock suitable for writing tests.
Parameters: - rate (float) – the initial
rate
. - autojump_threshold (float) – the initial
autojump_threshold
.
-
rate
¶ How many seconds of clock time pass per second of real time. Default is 0.0, i.e. the clock only advances through manuals calls to
jump()
or when theautojump_threshold
is triggered. You can assign to this attribute to change it.
-
autojump_threshold
¶ If all tasks are blocked for this many real seconds (i.e., according to the actual clock, not this clock), then this clock automatically jumps ahead to the run loop’s next scheduled timeout. Default is
math.inf
, i.e., to never autojump. You can assign to this attribute to change it.You should set this to the smallest value that lets you reliably avoid “false alarms” where some I/O is in flight (e.g. between two halves of a socketpair) but the threshold gets triggered and time gets advanced anyway. This will depend on the details of your tests and test environment. If you aren’t doing any I/O (like in our sleeping example above) then setting it to zero is fine.
Note that setting this attribute interacts with the run loop, so it can only be done from inside a run context or (as a special case) before calling
trio.run()
.Warning
If you’re using
wait_all_tasks_blocked()
andautojump_threshold
together, then you have to be careful. Settingautojump_threshold
acts like a task calling:while True: await wait_all_tasks_blocked(cushion=clock.autojump_threshold)
This means that if you call
wait_all_tasks_blocked()
with a cushion larger than your autojump threshold, then your call towait_all_tasks_blocked()
will never return, because the autojump task will keep waking up before your task does, and each time it does it’ll reset your task’s timer.Summary: you should set
autojump_threshold
to be at least as large as the largest cushion you plan to pass towait_all_tasks_blocked()
.
-
jump
(seconds)¶ Manually advance the clock by the given number of seconds.
Parameters: seconds (float) – the number of seconds to jump the clock forward. Raises: ValueError
– if you try to pass a negative value forseconds
.
- rate (float) – the initial
Inter-task ordering¶
-
class
trio.testing.
Sequencer
¶ A convenience class for forcing code in different tasks to run in an explicit linear order.
Instances of this class implement a
__call__
method which returns an async context manager. The idea is that you pass a sequence number to__call__
to say where this block of code should go in the linear sequence. Block 0 starts immediately, and then block N doesn’t start until block N-1 has finished.Example
An extremely elaborate way to print the numbers 0-5, in order:
async def worker1(seq): async with seq(0): print(0) async with seq(4): print(4) async def worker2(seq): async with seq(2): print(2) async with seq(5): print(5) async def worker3(seq): async with seq(1): print(1) async with seq(3): print(3) async def main(): seq = trio.testing.Sequencer() async with trio.open_nursery() as nursery: nursery.spawn(worker1, seq) nursery.spawn(worker2, seq) nursery.spawn(worker3, seq)
-
await
trio.testing.
wait_all_tasks_blocked
()¶ Block until there are no runnable tasks.
This is useful in testing code when you want to give other tasks a chance to “settle down”. The calling task is blocked, and doesn’t wake up until all other tasks are also blocked for at least
cushion
seconds. (Setting a non-zerocushion
is intended to handle cases like two tasks talking to each other over a local socket, where we want to ignore the potential brief moment between a send and receive when all tasks are blocked.)Note that
cushion
is measured in real time, not the trio clock time.If there are multiple tasks blocked in
wait_all_tasks_blocked()
, then the one with the shortestcushion
is the one woken (and the this task becoming unblocked resets the timers for the remaining tasks). If there are multiple tasks that have exactly the samecushion
, then all are woken.You should also consider
trio.testing.Sequencer
, which provides a more explicit way to control execution ordering within a test, and will often produce more readable tests.Example
Here’s an example of one way to test that trio’s locks are fair: we take the lock in the parent, spawn a child, wait for the child to be blocked waiting for the lock (!), and then check that we can’t release and immediately re-acquire the lock:
async def lock_taker(lock): await lock.acquire() lock.release() async def test_lock_fairness(): lock = trio.Lock() await lock.acquire() async with trio.open_nursery() as nursery: nursery.spawn(lock_taker, lock) # child hasn't run yet assert not lock.locked() await trio.testing.wait_all_tasks_blocked() # now the child has run assert lock.locked() lock.release() try: # The child has a prior claim, so we can't have it lock.acquire_nowait() except trio.WouldBlock: print("PASS") else: print("FAIL")
Testing checkpoints¶
-
with
trio.testing.
assert_yields
()¶ Use as a context manager to check that the code inside the
with
block executes at least one checkpoint.Raises: AssertionError
– if no checkpoint was executed.Example
Check that
trio.sleep()
is a checkpoint, even if it doesn’t block:with trio.testing.assert_yields(): await trio.sleep(0)
-
with
trio.testing.
assert_no_yields
()¶ Use as a context manager to check that the code inside the
with
block does not execute any check points.Raises: AssertionError
– if a checkpoint was executed.Example
Synchronous code never yields, but we can double-check that:
queue = trio.Queue(10) with trio.testing.assert_no_yields(): queue.put_nowait(None)
Low-level operations in trio.hazmat
¶
Warning
⚠️ DANGER DANGER DANGER ⚠️
You probably don’t want to use this module.
The trio.hazmat
API is public and stable (or at least, as
stable as anything in trio is!), but it has nasty
big pointy teeth. Mistakes may
not be handled gracefully; rules and conventions that are followed
strictly in the rest of trio do not always apply. Read and tread
carefully.
But if you find yourself needing to, for example, implement new synchronization primitives or expose new low-level I/O functionality, then you’re in the right place.
Low-level I/O primitives¶
Different environments expose different low-level APIs for performing
async I/O. trio.hazmat
attempts to expose these APIs in a
relatively direct way, so as to allow maximum power and flexibility
for higher level code. However, this means that the exact API provided
may vary depending on what system trio is running on.
Universally available API¶
All environments provide the following functions:
-
await
trio.hazmat.
wait_socket_readable
(sock)¶ Block until the given
socket.socket()
object is readable.The given object must be exactly of type
socket.socket()
, nothing else.Raises: - TypeError – if the given object is not of type
socket.socket()
. - RuntimeError – if another task is already waiting for the given socket to become readable.
- TypeError – if the given object is not of type
-
await
trio.hazmat.
wait_socket_writable
(sock)¶ Block until the given
socket.socket()
object is writable.The given object must be exactly of type
socket.socket()
, nothing else.Raises: - TypeError – if the given object is not of type
socket.socket()
. - RuntimeError – if another task is already waiting for the given socket to become writable.
- TypeError – if the given object is not of type
Unix-specific API¶
Unix-like systems provide the following functions:
-
await
trio.hazmat.
wait_readable
(fd)¶ Block until the given file descriptor is readable.
Warning
This is “readable” according to the operating system’s definition of readable. In particular, it probably won’t tell you anything useful for on-disk files.
Parameters: fd – integer file descriptor, or else an object with a fileno()
methodRaises: RuntimeError – if another task is already waiting for the given fd to become readable.
-
await
trio.hazmat.
wait_writable
(fd)¶ Block until the given file descriptor is writable.
Warning
This is “writable” according to the operating system’s definition of writable. In particular, it probably won’t tell you anything useful for on-disk files.
Parameters: fd – integer file descriptor, or else an object with a fileno()
methodRaises: RuntimeError – if another task is already waiting for the given fd to become writable.
System tasks¶
-
trio.hazmat.
spawn_system_task
()¶ Spawn a “system” task.
System tasks have a few differences from regular tasks:
- They don’t need an explicit nursery; instead they go into the internal “system nursery”.
- If a system task raises an exception, then it’s converted into a
TrioInternalError
and all tasks are cancelled. If you write a system task, you should be careful to make sure it doesn’t crash. - System tasks are automatically cancelled when the main task exits.
- By default, system tasks have
KeyboardInterrupt
protection enabled. If you want your task to be interruptible by control-C, then you need to usedisable_ki_protection()
explicitly.
Parameters: - async_fn – An async callable.
- args – Positional arguments for
async_fn
. If you want to pass keyword arguments, usefunctools.partial()
. - name – The name for this task. Only used for debugging/introspection
(e.g.
repr(task_obj)
). If this isn’t a string,spawn_system_task()
will try to make it one. A common use case is if you’re wrapping a function before spawning a new task, you might pass the original function as thename=
to make debugging easier.
Returns: the newly spawned task
Return type:
Entering trio from external threads or signal handlers¶
-
trio.hazmat.
current_call_soon_thread_and_signal_safe
()¶ Returns a reference to the
call_soon_thread_and_signal_safe
function for the current trio run:-
call_soon_thread_and_signal_safe
(sync_fn, *args, idempotent=False)¶ Schedule a call to
sync_fn(*args)
to occur in the context of a trio task. This is safe to call from the main thread, from other threads, and from signal handlers.The call is effectively run as part of a system task (see
spawn_system_task()
). In particular this means that:KeyboardInterrupt
protection is enabled by default; if you wantsync_fn
to be interruptible by control-C, then you need to usedisable_ki_protection()
explicitly.- If
sync_fn
raises an exception, then it’s converted into aTrioInternalError
and all tasks are cancelled. You should be careful thatsync_fn
doesn’t crash.
All calls with
idempotent=False
are processed in strict first-in first-out order.If
idempotent=True
, thensync_fn
andargs
must be hashable, and trio will make a best-effort attempt to discard any call submission which is equal to an already-pending call. Trio will make an attempt to process these in first-in first-out order, but no guarantees. (Currently processing is FIFO on CPython 3.6 and PyPy, but not CPython 3.5.)Any ordering guarantees apply separately to
idempotent=False
andidempotent=True
calls; there’s no rule for how calls in the different categories are ordered with respect to each other.Raises: trio.RunFinishedError – if the associated call to trio.run()
has already exited. (Any call that doesn’t raise this error is guaranteed to be fully processed beforetrio.run()
exits.)
-
Safer KeyboardInterrupt handling¶
Trio’s handling of control-C is designed to balance usability and
safety. On the one hand, there are sensitive regions (like the core
scheduling loop) where it’s simply impossible to handle arbitrary
KeyboardInterrupt
exceptions while maintaining our core
correctness invariants. On the other, if the user accidentally writes
an infinite loop, we do want to be able to break out of that. Our
solution is to install a default signal handler which checks whether
it’s safe to raise KeyboardInterrupt
at the place where the
signal is received. If so, then we do; otherwise, we schedule a
KeyboardInterrupt
to be delivered to the main task at the next
available opportunity (similar to how Cancelled
is
delivered).
So that’s great, but – how do we know whether we’re in one of the sensitive parts of the program or not?
This is determined on a function-by-function basis. By default, a function is protected if its caller is, and not if its caller isn’t; this is helpful because it means you only need to override the defaults at places where you transition from protected code to unprotected code or vice-versa.
These transitions are accomplished using two function decorators:
-
@
trio.hazmat.
disable_ki_protection
¶ Decorator that marks the given regular function, generator function, async function, or async generator function as unprotected against
KeyboardInterrupt
, i.e., the code inside this function can be rudely interrupted byKeyboardInterrupt
at any moment.If you have multiple decorators on the same function, then this should be at the bottom of the stack (closest to the actual function).
An example of where you’d use this is in implementing something like
run_in_trio_thread
, which usescall_soon_thread_and_signal_safe
to get into the trio thread.call_soon_thread_and_signal_safe
callbacks are run withKeyboardInterrupt
protection enabled, andrun_in_trio_thread
takes advantage of this to safely set up the machinery for sending a response back to the original thread, and then usesdisable_ki_protection()
when entering the user-provided function.
-
@
trio.hazmat.
enable_ki_protection
¶ Decorator that marks the given regular function, generator function, async function, or async generator function as protected against
KeyboardInterrupt
, i.e., the code inside this function won’t be rudely interrupted byKeyboardInterrupt
. (Though if it contains any checkpoints, then it can still receiveKeyboardInterrupt
at those. This is considered a polite interruption.)Warning
Be very careful to only use this decorator on functions that you know will either exit in bounded time, or else pass through a checkpoint regularly. (Of course all of your functions should have this property, but if you mess it up here then you won’t even be able to use control-C to escape!)
If you have multiple decorators on the same function, then this should be at the bottom of the stack (closest to the actual function).
An example of where you’d use this is on the
__exit__
implementation for something like aLock
, where a poorly-timedKeyboardInterrupt
could leave the lock in an inconsistent state and cause a deadlock.
-
trio.hazmat.
currently_ki_protected
()¶ Check whether the calling code has
KeyboardInterrupt
protection enabled.It’s surprisingly easy to think that one’s
KeyboardInterrupt
protection is enabled when it isn’t, or vice-versa. This function tells you what trio thinks of the matter, which makes it useful forassert
s and unit tests.Returns: True if protection is enabled, and False otherwise. Return type: bool
Sleeping and waking¶
Wait queue abstraction¶
-
class
trio.hazmat.
ParkingLot
¶ A fair wait queue with cancellation and requeueing.
This class encapsulates the tricky parts of implementing a wait queue. It’s useful for implementing higher-level synchronization primitives like queues and locks.
In addition to the methods below, you can use
len(parking_lot)
to get the number of parked tasks, andif parking_lot: ...
to check whether there are any parked tasks.-
await
park
()¶ Park the current task until woken by a call to
unpark()
orunpark_all()
.
-
await
unpark
(*, count=1)¶ Unpark one or more tasks.
This wakes up
count
tasks that are blocked inpark()
. If there are fewer thancount
tasks parked, then wakes as many tasks are available and then returns successfully.Parameters: count (int) – the number of tasks to unpark.
-
await
unpark_all
()¶ Unpark all parked tasks.
-
await
repark
(new_lot, *, count=1)¶ Move parked tasks from one
ParkingLot
object to another.This dequeues
count
tasks from one lot, and requeues them on another, preserving order. For example:async def parker(lot): print("sleeping") await lot.park() print("woken") async def main(): lot1 = trio.hazmat.ParkingLot() lot2 = trio.hazmat.ParkingLot() async with trio.open_nursery() as nursery: nursery.spawn(lot1) await trio.testing.wait_all_tasks_blocked() assert len(lot1) == 1 assert len(lot2) == 0 lot1.repark(lot2) assert len(lot1) == 0 assert len(lot2) == 1 # This wakes up the task that was originally parked in lot1 lot2.unpark()
If there are fewer than
count
tasks parked, then reparks as many tasks as are available and then returns successfully.Parameters: - new_lot (ParkingLot) – the parking lot to move tasks to.
- count (int) – the number of tasks to move.
-
await
repark_all
(new_lot)¶ Move all parked tasks from one
ParkingLot
object to another.See
repark()
for details.
-
await
Low-level checkpoint functions¶
-
await
trio.hazmat.
yield_briefly
()¶ A pure checkpoint.
This checks for cancellation and allows other tasks to be scheduled, without otherwise blocking.
Note that the scheduler has the option of ignoring this and continuing to run the current task if it decides this is appropriate (e.g. for increased efficiency).
Equivalent to
await trio.sleep(0)
(which is implemented by callingyield_briefly()
.)
The next two functions are used together to make up a checkpoint:
-
await
trio.hazmat.
yield_if_cancelled
()¶ A conditional checkpoint.
If a cancellation is active, then allows other tasks to be scheduled, and then raises
trio.Cancelled
.
-
await
trio.hazmat.
yield_briefly_no_cancel
()¶ Introduce a schedule point, but not a cancel point.
These are commonly used in cases where you have an operation that might-or-might-not block, and you want to implement trio’s standard checkpoint semantics. Example:
async def operation_that_maybe_blocks():
await yield_if_cancelled()
try:
ret = attempt_operation()
except BlockingIOError:
# need to block and then retry, which we do below
pass
except:
# some other error, finish the checkpoint then let it propagate
await yield_briefly_no_cancel()
raise
else:
# operation succeeded, finish the checkpoint then return
await yield_briefly_no_cancel()
return ret
while True:
await wait_for_operation_to_be_ready()
try:
return attempt_operation()
except BlockingIOError:
pass
This logic is a bit convoluted, but accomplishes all of the following:
- Every execution path passes through a checkpoint (assuming that
wait_for_operation_to_be_ready
is an unconditional checkpoint) - Our cancellation semantics say that
Cancelled
should only be raised if the operation didn’t happen. Usingyield_briefly_no_cancel()
on the early-exit branches accomplishes this. - On the path where we do end up blocking, we don’t pass through any schedule points before that, which avoids some unnecessary work.
- Avoids implicitly chaining the
BlockingIOError
with any errors raised byattempt_operation
orwait_for_operation_to_be_ready
, by keeping thewhile True:
loop outside of theexcept BlockingIOError:
block.
These functions can also be useful in other situations, e.g. if you’re
going to call an uncancellable operation like
trio.run_in_worker_thread()
or (potentially) overlapped I/O
operations on Windows, then you can call yield_if_cancelled()
first to make sure that the whole thing is a checkpoint.
Low-level blocking¶
-
class
trio.hazmat.
Abort
¶ enum.Enum
used as the return value from abort functions.See
yield_indefinitely()
for details.
-
trio.hazmat.
reschedule
()¶ Reschedule the given task with the given
Result
.See
yield_indefinitely()
for the gory details.There must be exactly one call to
reschedule()
for every call toyield_indefinitely()
. (And when counting, keep in mind that returningAbort.SUCCEEDED
from an abort callback is equivalent to callingreschedule()
once.)Parameters: - task (trio.Task) – the task to be rescheduled. Must be blocked in a
call to
yield_indefinitely()
. - next_send (trio.Result) – the value (or error) to return (or raise)
from
yield_indefinitely()
.
- task (trio.Task) – the task to be rescheduled. Must be blocked in a
call to
-
await
trio.hazmat.
yield_indefinitely
(abort_fn)¶ Put the current task to sleep, with cancellation support.
This is the lowest-level API for blocking in trio. Every time a
Task
blocks, it does so by calling this function.This is a tricky interface with no guard rails. If you can use
ParkingLot
or the built-in I/O wait functions instead, then you should.Generally the way it works is that before calling this function, you make arrangements for “someone” to call
reschedule()
on the current task at some later point.Then you call
yield_indefinitely()
, passing inabort_fn
, an “abort callback”.(Terminology: in trio, “aborting” is the process of attempting to interrupt a blocked task to deliver a cancellation.)
There are two possibilities for what happens next:
“Someone” calls
reschedule()
on the current task, andyield_indefinitely()
returns or raises whatever value or error was passed toreschedule()
.The call’s context transitions to a cancelled state (e.g. due to a timeout expiring). When this happens, the
abort_fn
is called. It’s interface looks like:def abort_fn(raise_cancel): ... return trio.hazmat.Abort.SUCCEEDED # or FAILED
It should attempt to clean up any state associated with this call, and in particular, arrange that
reschedule()
will not be called later. If (and only if!) it is successful, then it should returnAbort.SUCCEEDED
, in which case the task will automatically be rescheduled with an appropriateCancelled
error.Otherwise, it should return
Abort.FAILED
. This means that the task can’t be cancelled at this time, and still has to make sure that “someone” eventually callsreschedule()
.At that point there are again two possibilities. You can simply ignore the cancellation altogether: wait for the operation to complete and then reschedule and continue as normal. (For example, this is what
trio.run_in_worker_thread()
does if cancellation is disabled.) The other possibility is that theabort_fn
does succeed in cancelling the operation, but for some reason isn’t able to report that right away. (Example: on Windows, it’s possible to request that an async (“overlapped”) I/O operation be cancelled, but this request is also asynchronous – you don’t find out until later whether the operation was actually cancelled or not.) To report a delayed cancellation, then you should reschedule the task yourself, and call theraise_cancel
callback passed toabort_fn
to raise aCancelled
(or possiblyKeyboardInterrupt
) exception into this task. Either of the approaches sketched below can work:# Option 1: # Catch the exception from raise_cancel and inject it into the task. # (This is what trio does automatically for you if you return # Abort.SUCCEEDED.) trio.hazmat.reschedule(task, Result.capture(raise_cancel)) # Option 2: # wait to be woken by "someone", and then decide whether to raise # the error from inside the task. outer_raise_cancel = None def abort(inner_raise_cancel): nonlocal outer_raise_cancel outer_raise_cancel = inner_raise_cancel TRY_TO_CANCEL_OPERATION() return trio.hazmat.Abort.FAILED await yield_indefinitely(abort) if OPERATION_WAS_SUCCESSFULLY_CANCELLED: # raises the error outer_raise_cancel()
In any case it’s guaranteed that we only call the
abort_fn
at most once per call toyield_indefinitely()
.
Warning
If your
abort_fn
raises an error, or returns any value other thanAbort.SUCCEEDED
orAbort.FAILED
, then trio will crash violently. Be careful! Similarly, it is entirely possible to deadlock a trio program by failing to reschedule a blocked task, or cause havoc by callingreschedule()
too many times. Remember what we said up above about how you should use a higher-level API if at all possible?
Here’s an example lock class implemented using
yield_indefinitely()
directly. This implementation has a number
of flaws, including lack of fairness, O(n) cancellation, missing error
checking, failure to insert a checkpoint on the non-blocking path,
etc. If you really want to implement your own lock, then you should
study the implementation of trio.Lock
and use
ParkingLot
, which handles some of these issues for you. But
this does serve to illustrate the basic structure of the
yield_indefinitely()
API:
class NotVeryGoodLock:
def __init__(self):
self._blocked_tasks = collections.deque()
self._held = False
async def acquire(self):
while self._held:
task = trio.current_task()
self._blocked_tasks.append(task)
def abort_fn(_):
self._blocked_tasks.remove(task)
return trio.hazmat.Abort.SUCCEEDED
await trio.hazmat.yield_indefinitely(abort_fn)
self._held = True
def release(self):
self._held = False
if self._blocked_tasks:
woken_task = self._blocked_tasks.popleft()
trio.hazmat.reschedule(woken_task)
Design and internals¶
Here we’ll discuss Trio’s overall design and architecture: how it fits together and why we made the decisions we did. If all you want to do is use Trio, then you don’t need to read this – though you might find it interesting. The main target audience here is (a) folks who want to read the code and potentially contribute, (b) anyone working on similar libraries who want to understand what we’re up to, (c) anyone interested in I/O library design generally.
There are many valid approaches to writing an async I/O library. This is ours.
High-level design principles¶
Trio’s two overriding goals are usability and correctness: we want to make it easy to get things right.
Of course there are lots of other things that matter too, like speed, maintainability, etc. We want those too, as much as we can get. But sometimes these things come in conflict, and when that happens, these are our priorities.
In some sense the entire rest of this document is a description of how
these play out, but to give a simple example: Trio’s
KeyboardInterrupt
handling machinery is a bit tricky and hard to
test, so it scores poorly on simplicity and maintainability. But we
think the usability+correctness gains outweigh this.
There are some subtleties here. Notice that it’s specifically “easy to get things right”. There are situations (e.g. writing one-off scripts) where the most “usable” tool is the one that will happily ignore errors and keep going no matter what, or that doesn’t bother with resource cleanup. (Cf. the success of PHP.) This is a totally valid use case and valid definition of usability, but it’s not the one we use: we think it’s easier to build reliable and correct systems if exceptions propagate until handled and if the system catches you when you make potentially dangerous resource handling errors, so that’s what we optimize for.
It’s also worth saying something about speed, since it often looms large in comparisons between I/O libraries. This is a rather subtle and complex topic.
In general, speed is certainly important – but the fact that people sometimes use Python instead of C is a pretty good indicator that usability often trumps speed in practice. We want to make trio fast, but it’s not an accident that it’s left off our list of overriding goals at the top: if necessary we are willing to accept some slowdowns in the service of usability and reliability.
To break things down in more detail:
First of all, there are the cases where speed directly impacts
correctness, like when you hit an accidental O(N**2)
algorithm and
your program effectively locks up. Trio is very careful to use
algorithms and data structures that have good worst-case behavior
(even if this might mean sacrificing a few percentage points of speed
in the average case).
Similarly, when there’s a conflict, we care more about 99th percentile latencies than we do about raw throughput, because insufficient throughput – if it’s consistent! – can often be budgeted for and handled with horizontal scaling, but once you lose latency it’s gone forever, and latency spikes can easily cross over to become a correctness issue (e.g., an RPC server that responds slowly enough to trigger timeouts is effectively non-functional). Again, of course, this doesn’t mean we don’t care about throughput – but sometimes engineering requires making trade-offs, especially for early-stage projects that haven’t had time to optimize for all use cases yet.
And finally: we care about speed on real-world applications quite a bit, but speed on microbenchmarks is just about our lowest priority. We aren’t interested in competing to build “the fastest echo server in the West”. I mean, it’s nice if it happens or whatever, and microbenchmarks are an invaluable tool for understanding a system’s behavior. But if you play that game to win then it’s very easy to get yourself into a situation with seriously misaligned incentives, where you have to start compromising on features and correctness in order to get a speedup that’s totally irrelevant to real-world applications. In most cases (we suspect) it’s the application code that’s the bottleneck, and you’ll get more of a win out of running the whole app under PyPy than out of any heroic optimizations to the I/O layer. (And this is why Trio does place a priority on PyPy compatibility.)
As a matter of tactics, we also note that at this stage in Trio’s lifecycle, it’d probably be a mistake to worry about speed too much. It doesn’t make sense to spend lots of effort optimizing an API whose semantics are still in flux.
User-level API principles¶
Basic principles¶
Trio is very much a continuation of the ideas explored in this blog post, and in particular the principles identified there that make curio easier to use correctly than asyncio. So trio also adopts these rules, in particular:
- The only form of concurrency is the task.
- Tasks are guaranteed to run to completion.
- Task spawning is always explicit. No callbacks, no implicit concurrency, no futures/deferreds/promises/other APIs that involve callbacks. All APIs are “causal” except for those that are explicitly used for task spawning.
- Exceptions are used for error handling;
try
/finally
andwith
blocks for handling cleanup.
Cancel points and schedule points¶
The first major place that trio departs from curio is in its decision to make a much larger fraction of the API use sync functions rather than async functions, and to provide strong conventions about cancel points and schedule points. (At this point, there are a lot of ways that trio and curio have diverged. But this was really the origin – the tipping point where I realized that exploring these ideas would require a new library, and couldn’t be done inside curio.) The full reasoning here takes some unpacking.
First, some definitions: a cancel point is a point where your code
checks if it has been cancelled – e.g., due to a timeout having
expired – and potentially raises a Cancelled
error. A schedule
point is a point where the current task can potentially be suspended,
and another task allowed to run.
In curio, the convention is that all operations that interact with the run loop in any way are syntactically async, and it’s undefined which of these operations are cancel/schedule points; users are instructed to assume that any of them might be cancel/schedule points, but with a few exceptions there’s no guarantee that any of them are unless they actually block. (I.e., whether a given call acts as a cancel/schedule point is allowed to vary across curio versions and also depending on runtime factors like network load.)
But when using an async library, there are good reasons why you need to be aware of cancel and schedule points. They introduce a set of complex and partially conflicting constraints on your code:
You need to make sure that every task passes through a cancel point regularly, because otherwise timeouts become ineffective and your code becomes subject to DoS attacks and other problems. So for correctness, it’s important to make sure you have enough cancel points.
But... every cancel point also increases the chance of subtle
bugs in your program, because it’s a place where you have to be
prepared to handle a Cancelled
exception and clean up
properly. And while we try to make this as easy as possible,
these kinds of clean-up paths are notorious for getting missed
in testing and harboring subtle bugs. So the more cancel points
you have, the harder it is to make sure your code is correct.
Similarly, you need to make sure that every task passes through a schedule point regularly, because otherwise this task could end up hogging the event loop and preventing other code from running, causing a latency spike. So for correctness, it’s important to make sure you have enough schedule points.
But... you have to be careful here too, because every schedule point is a point where arbitrary other code could run, and alter your program’s state out from under you, introducing classic concurrency bugs. So as you add more schedule points, it becomes exponentially harder to reason about how your code is interleaved and be sure that it’s correct.
So an important question for an async I/O library is: how do we help the user manage these trade-offs?
Trio’s answer is informed by two further observations:
First, any time a task blocks (e.g., because it does an await
sock.recv()
but there’s no data available to receive), that
has to be a cancel point (because if the I/O never arrives, we
need to be able to time out), and it has to be a schedule point
(because the whole idea of asynchronous programming is that
when one task is waiting we can switch to another task to get
something useful done).
And second, a function which sometimes counts as a cancel/schedule point, and sometimes doesn’t, is the worst of both worlds: you have put in the effort to make sure your code handles cancellation or interleaving correctly, but you can’t count on it to help meet latency requirements.
With all that in mind, trio takes the following approach:
Rule 1: to reduce the number of concepts to keep track of, we collapse cancel points and schedule points together. Every point that is a cancel point is also a schedule point and vice versa. These are distinct concepts both theoretically and in the actual implementation, but we hide that distinction from the user so that there’s only one concept they need to keep track of.
Rule 2: Cancel+schedule points are determined statically. A trio primitive is either always a cancel+schedule point, or never a cancel+schedule point, regardless of runtime conditions. This is because we want it to be possible to determine whether some code has “enough” cancel/schedule points by reading the source code.
In fact, to make this even simpler, we require that this be determined without looking at the function arguments: each function is either a cancel+schedule point, or it isn’t.
Observation: rule 2 implies that any operation that sometimes blocks is always a cancel+schedule point.
So that gives us a number of cancel+schedule points. Are there any
others? Our answer is: no. It’s easy to add new points explicitly
(throw in a sleep(0)
or whatever) but hard to get rid of them when
you don’t want them. (And this is a real issue – “too many potential
cancel points” is definitely a tension I’ve felt
while trying to build things like task supervisors in curio.) And we
expect that most trio programs will execute potentially-blocking
operations “often enough” to produce reasonable behavior. So, rule 3:
the only cancel+schedule points are the potentially-blocking
operations.
And then there’s the question of how to effectively communicate this
information to the user. We want some way to mark out a category of
functions that might block or trigger a task switch, so that they’re
clearly distinguished from functions that don’t do this. Wouldn’t it
be nice if there were some Python feature, that naturally divided
functions into two categories, and maybe put some sort of special
syntactic marking on with the functions that can do weird things like
block and task switch...? Rule 4: in trio, only the potentially
blocking functions are async. So e.g. Event.wait()
is async, but
Event.set()
is sync.
Summing up: out of what’s actually a pretty vast space of design possibilities, we declare by fiat that when it comes to trio primitives, all of these categories are identical:
- async functions
- functions that can block
- functions where you need to be prepared to handle cancellation
- functions that are guaranteed to take care of checking for cancellation
- functions where you need to be prepared for a task switch
- functions that are guaranteed to take care of switching tasks if appropriate
This requires some non-trivial work internally – it actually takes a fair amount of care to make those 4 cancel/schedule categories line up, and there are some shenanigans required to let sync and async APIs interact with the run loop on an equal footing. But this is all invisible to the user, we feel that it pays off in terms of usability and correctness.
There is one exception to these rules, for async context
managers. Context managers are composed of two operations – enter and
exit – and sometimes only one of these is potentially
blocking. (Examples: async with lock:
can block when entering but
never when exiting; async with open_nursery() as ...:
can block
when exiting but never when entering.) But, Python doesn’t have
“half-asynchronous” context managers: either both operations are
async-flavored, or neither is. In Trio we take a pragmatic approach:
for this kind of async context manager, we enforce the above rules
only on the potentially blocking operation, and the other operation is
allowed to be syntactically async
but semantically
synchronous. And async context managers should always document which
of their operations are schedule+cancel points.
Exceptions always propagate¶
Another rule that trio follows is that exceptions must always propagate. This is like the zen line about “Errors should never pass silently”, except that in other concurrency libraries (Python threads, asyncio, curio, ...), it’s fairly common to end up with an undeliverable exception, which just gets printed to stderr and then discarded. While we understand the pragmatic constraints that motivated these libraries to adopt this approach, we feel that there are far too many situations where no human will ever look at stderr and notice the problem, and insist that trio APIs find a way to propagate exceptions “up the stack” – whatever that might mean.
This is often a challenging rule to follow – for example, the call soon code has to jump through some hoops to make it happen – but its most dramatic influence can seen in trio’s task-spawning interface, where it motivates the use of “nurseries”:
async def parent():
async with trio.open_nursery() as nursery:
nursery.spawn(child)
(See Tasks let you do multiple things at once for full details.)
If you squint you can see the influence of erlang’s “task linking” idea here, but it’s quite different in detail, exactly because Python has exceptions and Erlang doesn’t. Erlang’s links are symmetric and optional; to support exceptions we need ours to be asymmetric and mandatory.
This design also turns out to enforce a remarkable, unexpected invariant.
In the blog post
I called out a nice feature of curio’s spawning API, which is that
since spawning is the only way to break causality, and in curio
spawn
is async, this means that in curio sync functions are
guaranteed to be causal. One limitation though is that this invariant
is actually not very predictive: in curio there are lots of async
functions that could spawn off children and violate causality, but
most of them don’t, but there’s no clear marker for the ones that do.
Our API doesn’t quite give that guarantee, but actually a better one. In trio:
- Sync functions can’t create nurseries, because nurseries require an
async with
- Any async function can create a nursery and spawn new tasks... but creating a nursery allows task spawning without allowing causality breaking, because the children have to exit before the function is allowed to return. So we can preserve causality without having to give up concurrency!
- The only way to violate causality (which is an important feature, just one that needs to be handled carefully) is to explicitly create a nursery object in one task and then pass it into another task. And this provides a very clear and precise signal about where the funny stuff is happening – just watch for the nursery object getting passed around.
Introspection, debugging, testing¶
Tools for introspection and debugging are critical to achieving usability and correctness in practice, so they should be first-class considerations in trio.
Similarly, the availability of powerful testing tools has a huge impact on usability and correctness; we consider testing helpers to be very much in scope for the trio project.
Specific style guidelines¶
As noted above, functions that don’t block should be sync-colored, and functions that might block should be async-colored and unconditionally act as cancel+schedule points.
Any function that takes a callable to run should have a signature like:
def call_the_thing(fn, *args, kwonly1, kwonly2, ...):: ...
where
fn(*args)
is the thing to be called, andkwonly1
,kwonly2
, ... are keyword-only arguments that belong tocall_the_thing
. This applies even ifcall_the_thing
doesn’t take any arguments of its own, i.e. in this case its signature looks like:def call_the_thing(fn, *args):: ...
This allows users to skip faffing about with
functools.partial()
in most cases, while still providing an unambiguous and extensible way to pass arguments to the caller. (Hat-tip to asyncio, who we stole this convention from.)Whenever it makes sense, trio classes should have a method called
statistics()
which returns an immutable object with named fields containing internal statistics about the object that are useful for debugging or introspection (examples).Functions or methods whose purpose is to wait for a condition to become true should be called
wait_<condition>
. This avoids ambiguities like “doesawait readable()
check readability (returning a bool) or wait for readability?”.Sometimes this leads to the slightly funny looking
await wait_...
. Sorry. As far as I can tell all the alternatives are worse, and you get used to the convention pretty quick.If it’s desirable to have both blocking and non-blocking versions of a function, then they look like:
async def OPERATION(...): ... def OPERATION_nowait(...): ...
and the
nowait
version raisestrio.WouldBlock
if it would block.The word
monitor
is used for APIs that involve anUnboundedQueue
receiving some kind of events. (Examples: nursery.monitor
attribute, some of the low-level I/O functions intrio.hazmat
.)...we should, but currently don’t, have a solid convention to distinguish between functions that take an async callable and those that take a sync callable. See issue #68.
A brief tour of trio’s internals¶
If you want to understand how trio is put together internally, then
the first thing to know is that there’s a very strict internal
layering: the trio._core
package is a fully self-contained
implementation of the core scheduling/cancellation/IO handling logic,
and then the other trio.*
modules are implemented in terms of the
API it exposes. (If you want to see what this API looks like, then
import trio; print(trio._core.__all__)
). Everything exported from
trio._core
is also exported as part of either the trio
or
trio.hazmat
namespaces. (This is managed through the use of a
@_hazmat
decorator that marks which items in
trio._core.__all__
should go into trio.hazmat
.)
Rationale: currently, trio is a new project in a novel part of the
design space, so we don’t make any stability guarantees. But the goal
is to reach the point where we can declare the API stable. It’s
unlikely that we’ll be able to quickly explore all possible corners of
the design space and cover all possible types of I/O. So instead, our
strategy is to make sure that it’s possible for independent packages
to add new features on top of trio. Enforcing the trio
vs
trio._core
split is a way of eating our own dogfood: basic
functionality like trio.Queue
and trio.socket
is
actually implemented solely in terms of public APIs. And the hope is
that by doing this, we increase the chances that someone who comes up
with a better kind of queue or wants to add some new functionality
like, say, file system change watching, will be able to do that on top
of our public APIs without having to modify trio internals.
Inside trio._core
¶
There are three notable sub-modules that are largely independent of the rest of trio, and could (possibly should?) be extracted into their own independent packages:
_result.py
: DefinesResult
._multierror.py
: ImplementsMultiError
and associated infrastructure._ki.py
: Implements the core infrastructure for safe handling ofKeyboardInterrupt
.
The most important submodule, where everything is integrated, is
_run.py
. (This is also by far the largest submodule; it’d be nice
to factor bits of it out with possible, but it’s tricky because the
core functionality genuinely is pretty intertwined.) Notably, this is
where cancel scopes, nurseries, and Task
are defined; it’s
also where the scheduler state and trio.run()
live.
The one thing that isn’t in _run.py
is I/O handling. This is
delegated to an IOManager
class, of which there are currently
three implementations:
EpollIOManager
in_io_epoll.py
(used on Linux, Illuminos)KqueueIOManager
in_io_kqueue.py
(used on MacOS, *BSD)WindowsIOManager
in_io_windows.py
(used on Windows)
The epoll and kqueue backends take advantage of the epoll and kqueue
wrappers in the stdlib select
module. The windows backend uses
CFFI to access to the Win32 API directly (see
trio/_core/_windows_cffi.py
). In general, we prefer to go directly
to the raw OS functionality rather than use selectors
, for
several reasons:
- Controlling our own fate: I/O handling is pretty core to what trio
is about, and
selectors
is (as of 2017-03-01) somewhat buggy (e.g. issue 29587, issue 29255). Which isn’t a big deal on its own, but sinceselectors
is part of the standard library we can’t fix it and ship an updated version; we’re stuck with whatever we get. We want more control over our users’ experience than that. - Impedence mismatch: the
selectors
API isn’t particularly well-fitted to how we want to use it. For example, kqueue natively treats an interest in readability of some fd as a separate thing from an interest in that same fd’s writability, which neatly matches trio’s model.selectors.KqueueSelector
goes to some effort internally to lump together all interests in a single fd, and to use it we’d then we’d have to jump through more hoops to reverse this. Of course, the native epoll API is fd-centric in the same way as theselectors
API so we do still have to write code to jump through these hoops, but the point is that theselectors
abstractions aren’t providing a lot of extra value. - (Most important) Access to raw platform capabilities:
selectors
is highly inadequate on Windows, and even on Unix-like systems it hides a lot of power (e.g. kqueue can do a lot more than just check fd readability/writability!).
The IOManager
layer provides a fairly raw exposure of the capabilities
of each system, with public API functions that vary between different
backends. (This is somewhat inspired by how os
works.) These
public APIs are then exported as part of trio.hazmat
, and
higher-level APIs like trio.socket
abstract over these
system-specific APIs to provide a uniform experience.
Currently the choice of backend is made statically at import time, and there is no provision for “pluggable” backends. The intuition here is that we’d rather focus our energy on making one set of solid, official backends that provide a high-quality experience out-of-the-box on all supported systems.
Release history¶
v0.2.0 (????-??-??)¶
- New argument to
trio.run()
:restrict_keyboard_interrupt_to_checkpoints
.
v0.1.0 (2017-03-10)¶
- Initial release.