Concurrency in Python

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!

A few notes

when is it useful?

Common example: I/O

In python, we can think of concurrency as a way to "optimize" the main thread.

but concurrency is harder

So we try to make everything as synchronous as possible, and only add concurrency where it's needed.

2 of the different concurrency libraries in python

asyncio: provided by the standard library

trio: structured concurrency, the one you want to use.

2 different ways to start coroutines

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 stuff
import 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

This is problematic because the child can outlive the parent.

What if an exception is raised during the execution of the coroutine?

Structured concurrency

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

Communication between coroutines

shared state

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

shared state fixed

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)

Communicate by duplicating/giving state

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)

which one is better?

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.

CancelLation

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!

There are a lot of things i didn't talk about

Made with Slides.com