Curio Documentation

curio Documentation
Release 0.9
David Beazley
Dec 15, 2018 Contents
1.1 Curio - A Tutorial Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1Contents: 3
1.2 Curio How-To . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.3 Curio Reference Manual . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.4 Developing with Curio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
2Installation: 127
An Example 3129
Additional Features 4131
The Big Question: Why? 5133
Under the Covers 6135
Questions and Answers 7137
About 8139
Python Module Index 141 iii curio Documentation, Release 0.9
• a small and unusual object that is considered interesting or attractive
• A Python library for concurrent I/O and systems programming.
Curio is a library for performing concurrent I/O and common system programming tasks such as launching subprocesses and farming work out to thread and process pools. It uses Python coroutines and the explicit async/await syntax introduced in Python 3.5. Its programming model is based on cooperative multitasking and existing programming abstractions such as threads, sockets, files, subprocesses, locks, and queues. You’ll find it to be small and fast.
Contents 1curio Documentation, Release 0.9
2Contents CHAPTER
1
Contents:
1.1 Curio - A Tutorial Introduction
Curio is a library for performing concurrent I/O using Python coroutines and the async/await syntax introduced in
Python 3.5. Its programming model is based on existing system programming abstractions such as threads, sockets,
files, locks, and queues. Under the hood, it’s based on a task model that provides for advanced handling of cancellation, interesting interactions between threads and processes, and much more. It’s fun.
This tutorial will take you through the basics of creating and managing tasks in curio as well as some useful debugging features.
1.1.1 A Small Taste
Curio allows you to write concurrent I/O handling code that looks a lot like threads. For example, here is a simple echo server: from curio import run, spawn from curio.socket import
*async def echo_server(address): sock = socket(AF_INET, SOCK_STREAM) sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.bind(address) sock.listen(5) print('Server listening at', address) async with sock: while True: client, addr = await sock.accept() await spawn(echo_client, client, addr, daemon=True) async def echo_client(client, addr): print('Connection from', addr) async with client:
(continues on next page)
3
curio Documentation, Release 0.9
(continued from previous page) while True: data = await client.recv(1000) if not data: break await client.sendall(data) print('Connection closed') if __name__ == '__main__': run(echo_server, ('',25000))
This server can handle thousands of concurrent clients. It does not use threads. However, this example really doesn’t do Curio justice. In the rest of this tutorial, we’ll start with the basics and work our way back to this before jumping off the deep end into something more advanced.
1.1.2 Getting Started
Here is a simple curio hello world program–a task that prints a simple countdown as you wait for your kid to put their shoes on:
# hello.py import curio async def countdown(n): while n 0: print('T-minus', n) await curio.sleep(1) n -= 1 if __name__ == '__main__': curio.run(countdown, 10)
Run it and you’ll see a countdown. Yes, some jolly fun to be sure. Curio is based around the idea of tasks. Tasks are defined as coroutines using async functions. To make a task execute, it must run inside the curio kernel. The run() function starts the kernel with an initial task. The kernel runs until there are no more tasks to complete.
1.1.3 Tasks
Let’s add a few more tasks into the mix:
# hello.py import curio async def countdown(n): while n 0: print('T-minus', n) await curio.sleep(1) n -= 1 async def kid(): print('Building the Millenium Falcon in Minecraft') await curio.sleep(1000) async def parent():
(continues on next page)
4Chapter 1. Contents: curio Documentation, Release 0.9
(continued from previous page) kid_task = await curio.spawn(kid) await curio.sleep(5) print("Let's go") count_task = await curio.spawn(countdown, 10) await count_task.join() print("We're leaving!") await kid_task.join() print('Leaving') if __name__ == '__main__': curio.run(parent)
This program illustrates the process of creating and joining with tasks. Here, the parent() task uses the curio. spawn() coroutine to launch a new child task. After sleeping briefly, it then launches the countdown() task. The join() method is used to wait for a task to finish. In this example, the parent first joins with countdown() and then with kid() before trying to leave. If you run this program, you’ll see it produce the following output: bash % python3 hello.py
Building the Millenium Falcon in Minecraft
Let's go
T-minus 10
T-minus 9
T-minus 8
T-minus 7
T-minus 6
T-minus 5
T-minus 4
T-minus 3
T-minus 2
T-minus 1
We're leaving!
.... hangs ....
At this point, the program appears hung. The child is busy for the next 1000 seconds, the parent is blocked on join() and nothing much seems to be happening–this is the mark of all good concurrent programs (hanging that is). Change the last part of the program to run the kernel with the monitor enabled:
... if __name__ == '__main__': curio.run(parent, with_monitor=True)
Run the program again. You’d really like to know what’s happening? Yes? Open up another terminal window and connect to the monitor as follows: bash % python3 -m curio.monitor
Curio Monitor: 4 tasks running
Type help for commands curio
See what’s happening by typing ps: curio ps
Task State Cycles Timeout Sleep Task
------ ------------ ---------- ------- ------- ---------------------------------------
˓→-----------
(continues on next page)
1.1. Curio - A Tutorial Introduction 5
curio Documentation, Release 0.9
(continued from previous page)
None
None None
None None
None None
1FUTURE_WAIT 1Monitor.monitor_task
2READ_WAIT 1Kernel._run_coro. locals ._kernel_task
3TASK_JOIN 3parent
4TIME_SLEEP 1962.830 kid curio
In the monitor, you can see a list of the active tasks. You can see that the parent is waiting to join and that the kid is sleeping. Actually, you’d like to know more about what’s happening. You can get the stack trace of any task using the where command: curio w 3
Stack for Task(id=3, name='parent', state='TASK_JOIN') (most recent call last):
File "hello.py", line 23, in parent await kid_task.join() curio w 4
Stack for Task(id=4, name='kid', state='TIME_SLEEP') (most recent call last):
File "hello.py", line 12, in kid await curio.sleep(1000) curio
Actually, that kid is just being super annoying. Let’s cancel their world: curio cancel 4
Cancelling task 4
Connection closed by remote host
*** ***
This causes the whole program to die with a rather nasty traceback message like this:
Traceback (most recent call last):
File "/Users/beazley/Desktop/Projects/curio/curio/kernel.py", line 828, in _run_coro trap = current._throw(current.next_exc)
File "/Users/beazley/Desktop/Projects/curio/curio/task.py", line 95, in _task_runner return await coro
File "hello.py", line 12, in kid await curio.sleep(1000)
File "/Users/beazley/Desktop/Projects/curio/curio/task.py", line 440, in sleep return await _sleep(seconds, False)
File "/Users/beazley/Desktop/Projects/curio/curio/traps.py", line 80, in _sleep return (yield (_trap_sleep, clock, absolute)) curio.errors.TaskCancelled: TaskCancelled
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "hello.py", line 27, in module curio.run(parent, with_monitor=True, debug=())
File "/Users/beazley/Desktop/Projects/curio/curio/kernel.py", line 872, in run return kernel.run(corofunc, args, timeout=timeout)
*
File "/Users/beazley/Desktop/Projects/curio/curio/kernel.py", line 212, in run raise ret_exc
File "/Users/beazley/Desktop/Projects/curio/curio/kernel.py", line 825, in _run_coro trap = current._send(current.next_value)
File "/Users/beazley/Desktop/Projects/curio/curio/task.py", line 95, in _task_runner return await coro
File "hello.py", line 23, in parent await kid_task.join()
(continues on next page)
6Chapter 1. Contents: curio Documentation, Release 0.9
(continued from previous page)
File "/Users/beazley/Desktop/Projects/curio/curio/task.py", line 108, in join raise TaskError('Task crash') from self.next_exc curio.errors.TaskError: Task crash
Not surprisingly, the parent sure didn’t like having their child process abrubtly killed out of nowhere like that. The join() method returned with a TaskError exception to indicate that some kind of problem occurred in the child.
Debugging is an important feature of curio and by using the monitor, you see what’s happening as tasks run. You can
find out where tasks are blocked and you can cancel any task that you want. However, it’s not necessary to do this in the monitor. Change the parent task to include a timeout and some debugging print statements like this: async def parent(): kid_task = await curio.spawn(kid) await curio.sleep(5) print("Let's go") count_task = await curio.spawn(countdown, 10) await count_task.join() print("We're leaving!") try: await curio.timeout_after(10, kid_task.join) except curio.TaskTimeout: print('Where are you???') print(kid_task.traceback()) raise SystemExit() print('Leaving!')
If you run this version, the parent will wait 10 seconds for the child to join. If not, a debugging traceback for the child task is printed and the program quits. Use the traceback() method to see a traceback. Raising SystemExit() causes Curio to quit in the same manner as normal Python programs.
The parent could also elect to forcefully cancel the child. Change the program so that it looks like this: async def parent(): kid_task = await curio.spawn(kid) await curio.sleep(5) print("Let's go") count_task = await curio.spawn(countdown, 10) await count_task.join() print("We're leaving!") try: await curio.timeout_after(10, kid_task.join) except curio.TaskTimeout: print('I warned you!') await kid_task.cancel() print('Leaving!')
Of course, all is not lost in the child. If desired, they can catch the cancellation request and cleanup. For example: async def kid(): try: print('Building the Millenium Falcon in Minecraft') await curio.sleep(1000) except curio.CancelledError:
(continues on next page)
1.1. Curio - A Tutorial Introduction 7
curio Documentation, Release 0.9
(continued from previous page) print('Fine. Saving my work.') raise
Now your program should produce output like this: bash % python3 hello.py
Building the Millenium Falcon in Minecraft
Let's go
T-minus 10
T-minus 9
T-minus 8
T-minus 7
T-minus 6
T-minus 5
T-minus 4
T-minus 3
T-minus 2
T-minus 1
We're leaving!
I warned you!
Fine. Saving my work.
Leaving!
By now, you have the basic gist of the curio task model. You can create tasks, join tasks, and cancel tasks. Even if a task appears to be blocked for a long time, it can be cancelled by another task or a timeout. You have a lot of control over the environment.
1.1.4 Task Groups
What kind of kid plays Minecraft alone? Of course, they’re going to invite all of their school friends over. Change the kid() function like this: async def friend(name): print('Hi, my name is', name) print('Playing Minecraft') try: await curio.sleep(1000) except curio.CancelledError: print(name, 'going home') raise async def kid(): print('Building the Millenium Falcon in Minecraft') async with curio.TaskGroup() as f: await f.spawn(friend, 'Max') await f.spawn(friend, 'Lillian') await f.spawn(friend, 'Thomas') try: await curio.sleep(1000) except curio.CancelledError: print('Fine. Saving my work.') raise
In this code, the kid creates a task group and spawns a collection of tasks into it. Now you’ve got a four-fold problem of tasks sitting around doing nothing useful. You’d think the parent might have a problem with a motley crew like this,
8Chapter 1. Contents: curio Documentation, Release 0.9 but no. If you run the code again, you’ll get output like this:
Building the Millenium Falcon in Minecraft
Hi, my name is Max
Playing Minecraft
Hi, my name is Lillian
Playing Minecraft
Hi, my name is Thomas
Playing Minecraft
Let's go
T-minus 10
T-minus 9
T-minus 8
T-minus 7
T-minus 6
T-minus 5
T-minus 4
T-minus 3
T-minus 2
T-minus 1
We're leaving!
I warned you!
Fine. Saving my work.
Max going home
Lillian going home
Thomas going home
Leaving!
Carefully observe how all of those friends just magically went away. That’s the defining feature of a TaskGroup.
You can spawn tasks into a group and they will either all complete or they’ll all get cancelled if any kind of error occurs.
Either way, none of those tasks are executing when control-flow leaves the with-block. In this case, the cancellation of child() causes a cancellation to propagate to all of those friend tasks who promptly leave. Again, problem solved.
1.1.5 Task Synchronization
Although threads are not used to implement curio, you still might have to worry about task synchronization issues (e.g., if more than one task is working with mutable state). For this purpose, curio provides Event, Lock, Semaphore, and Condition objects. For example, let’s introduce an event that makes the child wait for the parent’s permission to start playing: start_evt = curio.Event() async def kid(): print('Can I play?') await start_evt.wait() print('Building the Millenium Falcon in Minecraft') async with curio.TaskGroup() as f: await f.spawn(friend, 'Max') await f.spawn(friend, 'Lillian') await f.spawn(friend, 'Thomas') try: await curio.sleep(1000) except curio.CancelledError: print('Fine. Saving my work.')
(continues on next page)
1.1. Curio - A Tutorial Introduction 9
curio Documentation, Release 0.9
(continued from previous page) raise async def parent(): kid_task = await curio.spawn(kid) await curio.sleep(5) print('Yes, go play') await start_evt.set() await curio.sleep(5) print("Let's go") count_task = await curio.spawn(countdown, 10) await count_task.join() print("We're leaving!") try: await curio.timeout_after(10, kid_task.join) except curio.TaskTimeout: print('I warned you!') await kid_task.cancel() print('Leaving!')
All of the synchronization primitives work the same way that they do in the threading module. The main difference is that all operations must be prefaced by await. Thus, to set an event you use await start_evt.set() and to wait for an event you use await start_evt.wait().
All of the synchronization methods also support timeouts. So, if the kid wanted to be rather annoying, they could use a timeout to repeatedly nag like this: async def kid(): while True: try: print('Can I play?') await curio.timeout_after(1, start_evt.wait) break except curio.TaskTimeout: print('Wha!?!') print('Building the Millenium Falcon in Minecraft') async with curio.TaskGroup() as f: await f.spawn(friend, 'Max') await f.spawn(friend, 'Lillian') await f.spawn(friend, 'Thomas') try: await curio.sleep(1000) except curio.CancelledError: print('Fine. Saving my work.') raise
1.1.6 Signals
What kind of screen-time obsessed helicopter parent lets their child and friends play Minecraft for a measly 5 seconds?
Instead, let’s have the parent allow the child to play as much as they want until a Unix signal arrives, indicating that it’s time to go. Modify the code to wait for Control-C or a SIGTERM using a SignalEvent like this:
10 Chapter 1. Contents: curio Documentation, Release 0.9 import signal async def parent(): goodbye = curio.SignalEvent(signal.SIGINT, signal.SIGTERM) kid_task = await curio.spawn(kid) await curio.sleep(5) print('Yes, go play') await start_evt.set() await goodbye.wait() print("Let's go") count_task = await curio.spawn(countdown, 10) await count_task.join() print("We're leaving!") try: await curio.timeout_after(10, kid_task.join) except curio.TaskTimeout: print('I warned you!') await kid_task.cancel() print('Leaving!')
If you run this program, you’ll get output like this:
Building the Millenium Falcon in Minecraft
Hi, my name is Max
Playing Minecraft
Hi, my name is Lillian
Playing Minecraft
Hi, my name is Thomas
Playing Minecraft
At this point, nothing is going to happen for awhile. The kids will play for the next 1000 seconds. However, if you press Control-C, you’ll see the program initiate it’s usual shutdown sequence:
^C (Control-C)
Let's go
T-minus 10
T-minus 9
T-minus 8
T-minus 7
T-minus 6
T-minus 5
T-minus 4
T-minus 3
T-minus 2
T-minus 1
We're leaving!
I warned you!
Fine. Saving my work.
Max going home
Lillian going home
Thomas going home
Leaving!
In either case, you’ll see the parent wake up, do the countdown and proceed to cancel the child. All the friends go
1.1. Curio - A Tutorial Introduction 11
curio Documentation, Release 0.9 home. Very good.
Signals are a weird affair though. Suppose that the parent discovers that the house is on fire and wants to get the kids out of there fast. As written, a SignalEvent captures the appropriate signal and sets a sticky flag. If the same signal comes in again, nothing much happens. In this code, the shutdown sequence would run to completion no matter how many times you hit Control-C. Everyone dies. Sadness.
This problem is easily solved–just delete the event after you’re done with it. Like this: async def parent(): goodbye = curio.SignalEvent(signal.SIGINT, signal.SIGTERM) kid_task = await curio.spawn(kid) await curio.sleep(5) print('Yes, go play') await start_evt.set() await goodbye.wait() del goodbye
# Removes the Control-C handler print("Let's go") count_task = await curio.spawn(countdown, 10) await count_task.join() print("We're leaving!") try: await curio.timeout_after(10, kid_task.join) except curio.TaskTimeout: print('I warned you!') await kid_task.cancel() print('Leaving!')
Run the program again. Now, quickly hit Control-C twice in a row. Boom! Minecraft dies instantly and everyone hurries their way out of there. You’ll see the friends, the child, and the parent all making a hasty exit.
1.1.7 Number Crunching and Blocking Operations
Now, suppose for a moment that the kid has discovered that the shape of the Millenium Falcon is based on the Golden
Ratio and that building it now requires computing a sum of larger and larger Fibonacci numbers using an exponential algorithm like this: def fib(n): if n 2: return 1 else: return fib(n-1) + fib(n-2) async def kid(): while True: try: print('Can I play?') await curio.timeout_after(1, start_evt.wait) break except curio.TaskTimeout: print('Wha!?!') print('Building the Millenium Falcon in Minecraft')
(continues on next page)
12 Chapter 1. Contents: curio Documentation, Release 0.9
(continued from previous page) async with curio.TaskGroup() as f: await f.spawn(friend, 'Max') await f.spawn(friend, 'Lillian') await f.spawn(friend, 'Thomas') try: total = 0 for n in range(50): total += fib(n) except curio.CancelledError: print('Fine. Saving my work.') raise
If you run this version, you’ll find that the entire kernel becomes unresponsive. For example, signals aren’t caught and there appears to be no way to get control back. The problem here is that the kid is hogging the CPU and never yields.
Important lesson: curio DOES NOT provide preemptive scheduling. If a task decides to compute large Fibonacci numbers or mine bitcoins, everything will block until it’s done. Don’t do that.
If you’re trying to debug a situation like this, the good news is that you can still use the Curio monitor to find out what’s happening. For example, you could start a separate terminal window and type this: bash % python3 -m curio.monitor
Curio Monitor: 7 tasks running
Type help for commands curio ps
Task State Cycles Timeout Sleep Task
------ ------------ ---------- ------- ------- ---------------------------------------
˓→-----------
None None
None None
None None
None None
None None
None None
None None
1FUTURE_WAIT 1Monitor.monitor_task
2READ_WAIT 1Kernel._run_coro. locals ._kernel_task
3FUTURE_WAIT 2parent
4RUNNING 6kid
5READY 0friend
READY 60friend
READY 70friend curio w 4
Stack for Task(id=4, name='kid', state='RUNNING') (most recent call last):
File "hello.py", line 44, in kid total += fib(n) curio signal SIGKILL
Connection closed by remote host
bash %
*** ***
The bad news is that if you want other tasks to run, you’ll have to figure out some other way to carry out computationally intensive work. If you know that the work might take awhile, you can have it execute in a separate process.
Change the code to use curio.run_in_process() like this: async def kid(): while True: try: print('Can I play?') await curio.timeout_after(1, start_evt.wait) break except curio.TaskTimeout: print('Wha!?!')
(continues on next page)
1.1. Curio - A Tutorial Introduction 13
curio Documentation, Release 0.9
(continued from previous page) print('Building the Millenium Falcon in Minecraft') async with curio.TaskGroup() as f: await f.spawn(friend, 'Max') await f.spawn(friend, 'Lillian') await f.spawn(friend, 'Thomas') try: total = 0 for n in range(50): total += await curio.run_in_process(fib, n) except curio.CancelledError: print('Fine. Saving my work.') raise