Slide 1

Slide 1 text

TRIO STRUCTURED CONCURRENCY FOR PYTHON Jeremy Thurgood PyConZA 2020 1

Slide 2

Slide 2 text

A LITTLE HISTORY PRELUDE 2

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

ABOUT CONCURRENCY EXPOSITION 7

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

USING TRIO DEVELOPMENT 13

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

CONCLUSION CODA 21

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

APPLAUSE!!! THE END 23

Slide 24

Slide 24 text

QUESTIONS? 24