Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Trio: Structured Concurrency for Python by Jeremy Thurgood

Pycon ZA
October 10, 2020

Trio: Structured Concurrency for Python by Jeremy Thurgood

Concurrency has a reputation for being complicated and hard to get right, even in Python. Fortunately, by using the "structured concurrency" programming model, it's possible to avoid many of the pitfalls inherent in more traditional thread-based and callback-based models.

Trio [1] is an async I/O framework that brings the benefits of structured concurrency to Python. Built from the ground up to use async/await natively, Trio makes it possible to write async software that is robust and easy to reason about.

In this talk I'll explain how Trio differs from its predecessors (such as Twisted and asyncio), show that it leads to simpler code with fewer bugs, and hopefully convince you to give it try.

[1] https://trio.readthedocs.io/

Pycon ZA

October 10, 2020
Tweet

More Decks by Pycon ZA

Other Decks in Programming

Transcript

  1. TRIO
    STRUCTURED CONCURRENCY
    FOR PYTHON
    Jeremy Thurgood
    PyConZA 2020
    1

    View Slide

  2. A LITTLE HISTORY
    PRELUDE
    2

    View Slide

  3. THE ERA OF GOTO
    10 I = 1
    20 PRINT "LINE NUMBER"; I
    30 I = I + 1
    40 IF I <= 10 GOTO 20
    50 PRINT "ALL DONE"
    3

    View Slide

  4. MORE COMPLICATED GOTO
    10 INPUT "HOW MANY PRIMES"; N : DIM P(N-1)
    20 PRINT 2 : PRINT 3
    30 P(1) = 3 : PI = 1
    40 C = P(PI)
    50 C = C+2 : I = 0
    60 I = I+1
    70 IF C/P(I) = INT(C/P(I)) GOTO 50
    80 IF I < PI GOTO 60
    90 PRINT C : PI = PI+1 : P(PI) = C
    100 IF PI < N-2 GOTO 40
    4

    View Slide

  5. STRUCTURED PROGRAMMING
    Ten lines:
    for i in range(10):
    print("LINE NUMBER", i+1)
    print("ALL DONE")
    1
    2
    3
    Prime Numbers:
    n = int(input("HOW MANY PRIMES? "))
    print(2)
    primes = []
    candidate = 3
    while len(primes) < n-1:
    for p in primes:
    if candidate % p == 0:
    break
    else: # Runs if the loop ends without breaking.
    primes.append(candidate)
    print(candidate)
    candidate += 2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    5

    View Slide

  6. CONSEQUENCES OF STRUCTURE
    Execution ow that humans can reason about
    Call stacks and local variables
    Exceptions and error handling
    Context managers and with blocks
    6

    View Slide

  7. ABOUT CONCURRENCY
    EXPOSITION
    7

    View Slide

  8. WHAT IS CONCURRENCY?
    Threads! Promises! Mutexes! Deadlocks!
    Multiple independent ows of execution
    Communication between ows
    8

    View Slide

  9. spawn: goto FOR CONCURRENCY
    (Don’t worry too much about the syntax. Erlang is weird.)
    %% This is a recursive function with two clauses.
    %% It counts backwards because that's less code.
    say(_, 0) ->
    ok;
    say(Msg, N) ->
    io:fwrite("~w ~s~n", [N, Msg]),
    timer:sleep(1000),
    say(Msg, N-1).
    main(_) ->
    spawn(fun() -> say("task1", 5) end),
    spawn(fun() -> say("task2", 5) end),
    timer:sleep(6000).
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    say(_, 0) ->
    ok;
    say(Msg, N) ->
    io:fwrite("~w ~s~n", [N, Msg]),
    timer:sleep(1000),
    say(Msg, N-1).
    %% This is a recursive function with two clauses.
    1
    %% It counts backwards because that's less code.
    2
    3
    4
    5
    6
    7
    8
    9
    main(_) ->
    10
    spawn(fun() -> say("task1", 5) end),
    11
    spawn(fun() -> say("task2", 5) end),
    12
    timer:sleep(6000).
    13
    main(_) ->
    spawn(fun() -> say("task1", 5) end),
    spawn(fun() -> say("task2", 5) end),
    timer:sleep(6000).
    %% This is a recursive function with two clauses.
    1
    %% It counts backwards because that's less code.
    2
    say(_, 0) ->
    3
    ok;
    4
    say(Msg, N) ->
    5
    io:fwrite("~w ~s~n", [N, Msg]),
    6
    timer:sleep(1000),
    7
    say(Msg, N-1).
    8
    9
    10
    11
    12
    13
    %% This is a recursive function with two clauses.
    %% It counts backwards because that's less code.
    say(_, 0) ->
    ok;
    say(Msg, N) ->
    io:fwrite("~w ~s~n", [N, Msg]),
    timer:sleep(1000),
    say(Msg, N-1).
    main(_) ->
    spawn(fun() -> say("task1", 5) end),
    spawn(fun() -> say("task2", 5) end),
    timer:sleep(6000).
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    9

    View Slide

  10. ASYNCIO CALLS IT create_task
    import asyncio
    async def print_lines(msg, n):
    for i in range(n):
    print(i+1, msg)
    await asyncio.sleep(0.2)
    async def main():
    await print_lines("basically sync", 3)
    t1 = asyncio.create_task(print_lines("task1", 3))
    t2 = asyncio.create_task(print_lines("task2", 6))
    await t1
    await t2
    asyncio.run(main())
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    async def print_lines(msg, n):
    for i in range(n):
    print(i+1, msg)
    await asyncio.sleep(0.2)
    import asyncio
    1
    2
    3
    4
    5
    6
    7
    async def main():
    8
    await print_lines("basically sync", 3)
    9
    t1 = asyncio.create_task(print_lines("task1", 3))
    10
    t2 = asyncio.create_task(print_lines("task2", 6))
    11
    await t1
    12
    await t2
    13
    14
    asyncio.run(main())
    15
    async def main():
    await print_lines("basically sync", 3)
    t1 = asyncio.create_task(print_lines("task1", 3))
    t2 = asyncio.create_task(print_lines("task2", 6))
    await t1
    await t2
    import asyncio
    1
    2
    async def print_lines(msg, n):
    3
    for i in range(n):
    4
    print(i+1, msg)
    5
    await asyncio.sleep(0.2)
    6
    7
    8
    9
    10
    11
    12
    13
    14
    asyncio.run(main())
    15
    import asyncio
    async def print_lines(msg, n):
    for i in range(n):
    print(i+1, msg)
    await asyncio.sleep(0.2)
    async def main():
    await print_lines("basically sync", 3)
    t1 = asyncio.create_task(print_lines("task1", 3))
    t2 = asyncio.create_task(print_lines("task2", 6))
    await t1
    await t2
    asyncio.run(main())
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    10

    View Slide

  11. WHERE DO ERRORS GO?
    import asyncio
    async def raise_error(msg):
    await asyncio.sleep(0.1)
    raise Exception(msg)
    async def main():
    t1 = asyncio.create_task(raise_error("task1")) # Never awaited.
    t2 = asyncio.create_task(raise_error("task2")) # Awaited later.
    await asyncio.create_task(raise_error("task3")) # Awaited now.
    await t2
    asyncio.run(main())
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    t1 = asyncio.create_task(raise_error("task1")) # Never awaited.
    import asyncio
    1
    2
    async def raise_error(msg):
    3
    await asyncio.sleep(0.1)
    4
    raise Exception(msg)
    5
    6
    async def main():
    7
    8
    t2 = asyncio.create_task(raise_error("task2")) # Awaited later.
    9
    await asyncio.create_task(raise_error("task3")) # Awaited now.
    10
    await t2
    11
    12
    asyncio.run(main())
    13
    t2 = asyncio.create_task(raise_error("task2")) # Awaited later.
    await t2
    import asyncio
    1
    2
    async def raise_error(msg):
    3
    await asyncio.sleep(0.1)
    4
    raise Exception(msg)
    5
    6
    async def main():
    7
    t1 = asyncio.create_task(raise_error("task1")) # Never awaited.
    8
    9
    await asyncio.create_task(raise_error("task3")) # Awaited now.
    10
    11
    12
    asyncio.run(main())
    13
    await asyncio.create_task(raise_error("task3")) # Awaited now.
    import asyncio
    1
    2
    async def raise_error(msg):
    3
    await asyncio.sleep(0.1)
    4
    raise Exception(msg)
    5
    6
    async def main():
    7
    t1 = asyncio.create_task(raise_error("task1")) # Never awaited.
    8
    t2 = asyncio.create_task(raise_error("task2")) # Awaited later.
    9
    10
    await t2
    11
    12
    asyncio.run(main())
    13
    import asyncio
    async def raise_error(msg):
    await asyncio.sleep(0.1)
    raise Exception(msg)
    async def main():
    t1 = asyncio.create_task(raise_error("task1")) # Never awaited.
    t2 = asyncio.create_task(raise_error("task2")) # Awaited later.
    await asyncio.create_task(raise_error("task3")) # Awaited now.
    await t2
    asyncio.run(main())
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    11

    View Slide

  12. STRUCTURED CONCURRENCY
    Tasks spawned in a block must nish in that block
    Execution ow that humans can reason about
    Nested contexts and task-local state
    Well-de ned cancellation and error handling
    No orphaned tasks or missing results
    12

    View Slide

  13. USING TRIO
    DEVELOPMENT
    13

    View Slide

  14. READ THE DOCS
    https://trio.readthedocs.io/
    No, seriously, the docs are excellent
    Okay, I’ll talk about trio a bit more
    14

    View Slide

  15. FROM THE VERY BEGINNING
    Async code can call sync code
    Sync code can’t call async code
    import trio
    def square(n):
    return n * n
    async def cube(n):
    return n * square(n)
    trio.run(cube, 2)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    … but don’t call anything that blocks
    … but frameworks like Trio are a special case
    15

    View Slide

  16. GETTING IT WRONG
    Debugging a forgotten await is annoying.

    Trio provides some debugging tools:
    trio.abc.Instrument
    import trio
    async def slow_square(n):
    await trio.sleep(0.2)
    return n * n
    def foo():
    print(await slow_square(3)) # SyntaxError
    async def bar():
    print(slow_square(3)) # RuntimeWarning logged
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    print(await slow_square(3)) # SyntaxError
    import trio
    1
    2
    async def slow_square(n):
    3
    await trio.sleep(0.2)
    4
    return n * n
    5
    6
    def foo():
    7
    8
    9
    async def bar():
    10
    print(slow_square(3)) # RuntimeWarning logged
    11 print(slow_square(3)) # RuntimeWarning logged
    import trio
    1
    2
    async def slow_square(n):
    3
    await trio.sleep(0.2)
    4
    return n * n
    5
    6
    def foo():
    7
    print(await slow_square(3)) # SyntaxError
    8
    9
    async def bar():
    10
    11
    16

    View Slide

  17. NURSERIES: ACTUAL CONCURRENCY
    A nursery block doesn’t exit until all child tasks are
    complete.
    import trio
    async def print_lines(msg, n):
    for i in range(n):
    print(i+1, msg)
    await trio.sleep(0.2)
    print("task finished:", msg)
    async def main():
    print("parent: started!")
    async with trio.open_nursery() as nursery:
    nursery.start_soon(print_lines, "task1", 4)
    nursery.start_soon(print_lines, "task2", 2)
    print("parent: waiting for tasks to finish...")
    print("parent: all done!")
    trio.run(main)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    async def print_lines(msg, n):
    for i in range(n):
    print(i+1, msg)
    await trio.sleep(0.2)
    print("task finished:", msg)
    import trio
    1
    2
    3
    4
    5
    6
    7
    8
    async def main():
    9
    print("parent: started!")
    10
    async with trio.open_nursery() as nursery:
    11
    nursery.start_soon(print_lines, "task1", 4)
    12
    nursery.start_soon(print_lines, "task2", 2)
    13
    print("parent: waiting for tasks to finish...")
    14
    print("parent: all done!")
    15
    16
    trio.run(main)
    17
    async with trio.open_nursery() as nursery:
    nursery.start_soon(print_lines, "task1", 4)
    nursery.start_soon(print_lines, "task2", 2)
    print("parent: waiting for tasks to finish...")
    import trio
    1
    2
    async def print_lines(msg, n):
    3
    for i in range(n):
    4
    print(i+1, msg)
    5
    await trio.sleep(0.2)
    6
    print("task finished:", msg)
    7
    8
    async def main():
    9
    print("parent: started!")
    10
    11
    12
    13
    14
    print("parent: all done!")
    15
    16
    trio.run(main)
    17
    17

    View Slide

  18. TIMEOUTS
    import trio
    async def main():
    with trio.move_on_after(3):
    print("Went to sleep")
    await trio.sleep(2)
    print("Still sleeping")
    await trio.sleep(2)
    print("Woke up on my own")
    print("Nap time is over")
    try:
    with trio.fail_after(2):
    async with open_nursery() as n:
    n.start_soon(trio.sleep, 1)
    n.start_soon(trio.sleep, 3)
    await trio.sleep(4)
    except trio.TooSlowError as e:
    print(f"Took too long: {e!r}")
    trio.run(main)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    with trio.move_on_after(3):
    print("Went to sleep")
    await trio.sleep(2)
    print("Still sleeping")
    await trio.sleep(2)
    print("Woke up on my own")
    print("Nap time is over")
    import trio
    1
    2
    async def main():
    3
    4
    5
    6
    7
    8
    9
    10
    11
    try:
    12
    with trio.fail_after(2):
    13
    async with open_nursery() as n:
    14
    n.start_soon(trio.sleep, 1)
    15
    n.start_soon(trio.sleep, 3)
    16
    await trio.sleep(4)
    17
    except trio.TooSlowError as e:
    18
    print(f"Took too long: {e!r}")
    19
    20
    trio.run(main)
    21
    try:
    with trio.fail_after(2):
    async with open_nursery() as n:
    n.start_soon(trio.sleep, 1)
    n.start_soon(trio.sleep, 3)
    await trio.sleep(4)
    except trio.TooSlowError as e:
    print(f"Took too long: {e!r}")
    import trio
    1
    2
    async def main():
    3
    with trio.move_on_after(3):
    4
    print("Went to sleep")
    5
    await trio.sleep(2)
    6
    print("Still sleeping")
    7
    await trio.sleep(2)
    8
    print("Woke up on my own")
    9
    print("Nap time is over")
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    trio.run(main)
    21
    import trio
    async def main():
    with trio.move_on_after(3):
    print("Went to sleep")
    await trio.sleep(2)
    print("Still sleeping")
    await trio.sleep(2)
    print("Woke up on my own")
    print("Nap time is over")
    try:
    with trio.fail_after(2):
    async with open_nursery() as n:
    n.start_soon(trio.sleep, 1)
    n.start_soon(trio.sleep, 3)
    await trio.sleep(4)
    except trio.TooSlowError as e:
    print(f"Took too long: {e!r}")
    trio.run(main)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    18

    View Slide

  19. ERRORS AND CANCELLATION
    import trio
    async def print_lines(msg, n):
    for i in range(n):
    print(i+1, msg)
    await trio.sleep(0.4)
    async def timebomb(delay):
    await trio.sleep(delay)
    raise Exception("KABOOOM!")
    async def main():
    try:
    async with trio.open_nursery() as n:
    n.start_soon(print_lines, "child", 5)
    n.start_soon(timebomb, 1)
    await print_lines("body", 5)
    except Exception as e:
    print(f"Error: {e}")
    trio.run(main)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    19

    View Slide

  20. OTHER FEATURES
    Task-local storage
    Communication between tasks
    Events
    Channels
    Locks, semaphores, …
    Async generators (with caveats)
    Threads (if you must)
    Async lesystem operations
    Subprocesses
    20

    View Slide

  21. CONCLUSION
    CODA
    21

    View Slide

  22. WHAT HAVE WE LEARNED?
    goto is bad (but we already knew this)
    spawn is bad, no matter how you spell it
    Structured Concurrency is great
    Use Trio for your next async project
    22

    View Slide

  23. APPLAUSE!!!
    THE END
    23

    View Slide

  24. QUESTIONS?
    24

    View Slide