Synchronous
Concurrent
Parallel
How do you know when to switch?
You don't decide which coroutine the scheduler will go to.
Don't keep the control to yourself!
Common example: I/O
In python, we can think of concurrency as a way to "optimize" the main thread.
So we try to make everything as synchronous as possible, and only add concurrency where it's needed.
asyncio: provided by the standard library
trio: structured concurrency, the one you want to use.
In python, a coroutine is just a regular function, with one special ability: it can tell the scheduler to take control again.
Parent coroutine
Child coroutine
Just like a regular function call
async def parent():
await child()Parent coroutine
Child coroutine
async def parent(scheduler):
scheduler.create_task(child())
# do some more stuffimport asyncio
async def count(name):
for i in range(5):
print(name, i)
await asyncio.sleep(0.1)
async def main(scheduler):
await count("first")
scheduler.create_task(count("second"))
scheduler.create_task(count("third"))
print("DONE!")
scheduler = asyncio.get_event_loop()
scheduler.create_task(main(scheduler))
scheduler.run_forever()
# first 0
# first 1
# first 2
# first 3
# first 4
# DONE!
# second 0
# third 0
# second 1
# third 1
# second 2
# third 2
# second 3
# third 3
# second 4
# third 4
What if an exception is raised during the execution of the coroutine?
import trio
async def main():
async with trio.open_nursery() as nursery:
nursery.start_soon(coroutine1)
nursery.start_soon(coroutine2)
# at this point, both coroutine1 and coroutine2 have finished
trio.run(main)Parent coroutine
Start nursery
Nursery finished
Nurseries can be nested
import trio
async def render_game(state):
while True:
for player in state.values():
await player.render()
await trio.sleep(0)
async def update_state(stream, state):
while True:
update = await stream.read()
for username in update['gone']:
del state[username]
# update the state some more
async def main():
state = {
# username: player
}
stream = await open_connection()
async with trio.open_nursery() as nursery:
nursery.start_soon(render_game, state)
nursery.start_soon(update_state, stream, state)
trio.run(main)
This is BROKEN! Two coroutines write/read the same variable at the same time
import trio
async def render_game(lock, state):
while True:
async with lock:
for player in state.values():
await player.render()
async def update_state(stream, lock, state):
while True:
update = await stream.read()
async with lock:
for username in update['gone']:
del state[username]
# update the state some more
async def main():
state = {
# username: player
}
lock = trio.Lock()
stream = await open_connection()
async with trio.open_nursery() as nursery:
nursery.start_soon(render_game, lock, state)
nursery.start_soon(update_state, stream, lock, state)
trio.run(main)
Channels
Close a channel
Give up ownership of the data you send
import trio
async def main():
sendch, recvch = trio.open_memory_channel(0)
async with trio.open_nursery() as nursery:
nursery.start_soon(sender, sendch)
nursery.start_soon(receiver2, recvch)
async def sender(sendch):
await sendch.send("hello")
await sendch.send("bye bye")
await sendch.aclose()
async def receiver(recvch):
async for item in recvch:
print("got", item)
print("channel closed!")
async def receiver2(recvch):
print("first", await recvch.receive())
print("second", await recvch.receive())
try:
await recvch.receive()
except trio.EndOfChannel:
print("Channel closed!")
trio.run(main)
Duplicating/giving your state by sending it on a channel is usually preferred.
But sometimes we need locks to protect resources that can't be duplicated.
Graceful cancel
Hard cancel
import trio
async def main():
async with trio.move_on_after(2) as cancel_scope:
await coroutine1()
if cancel_scope.cancelled_caught:
print("coroutine cancelled!")You need to give back control to the scheduler so that it can cancel the coroutine!
Go to trio.readthedocs.io