├── README.md
├── conference_slides.pdf
├── cooking.md
├── coroutines.md
├── examples
├── animals.py
├── animals_asyncio.py
├── byhand_asyncio.py
├── byhand_coroutines_manual_sleep.py
├── byhand_coroutines_time_sleep.py
├── byhand_functions.py
├── gettoknow_exceptions.py
├── gettoknow_executor.py
├── gettoknow_forever.py
├── gettoknow_tasks.py
├── pubsub_aiohttp.py
└── webservice_animals_aiottp.py
└── media
├── beans.jpg
├── dinner.jpg
├── rice.jpg
└── salmon.jpg
/README.md:
--------------------------------------------------------------------------------
1 | 🚧 *WARNING: THIS REPO IS A HOT MESS - WILL FIX IT UP
2 | AFTER I GET HOME!* 🚧
3 |
4 | # A Brief Introduction to Concurrency and Coroutines
5 |
6 | This is the repository of links and supporting materials for my
7 | [PyOhio 2017 tutorial](https://pyohio.org/schedule/presentation/289/).
8 |
9 | Initially I had hoped to provide a full tutorial in written form suitable
10 | for studying offline, but
11 | that has proven to be too ambitious given the time I have available, so
12 | I will be focusing on preparing the live presentation.
13 |
14 | ## Abstract
15 |
16 | This is a beginner-friendly introduction to concurrent programming using python
17 | coroutines and the standard library asyncio module.
18 | The tutorial will first cover the meaning of concurrent programming
19 | and how it differs from parallelism.
20 | It will then continue to explore the syntax introduced in python 3.5:
21 | coroutine definitions, await expressions, async for statements, and
22 | async with statements.
23 | The need for a framework with a scheduler or event loop will be discussed,
24 | with the standard library asyncio package used as an example for the tutorial.
25 |
26 | A simple slow web service will be introduced as an example for understanding
27 | how to use coroutines. We will write a simple client to make several
28 | requests in sequence, and then use the aiohttp library to rewrite it using
29 | coroutines to make concurrent requests.
30 | Time permitting, we will also look at rewriting the web service itself
31 | to use coroutines to handle multiple requests concurrently.
32 |
33 | It will be assumed that the listener has a basic understanding of
34 | functions, classes, and exceptions in Python, but no prior knowledge
35 | of concurrent programming is required.
36 |
37 | ## Setup Instructions
38 |
39 | This tutorial requires Python 3.5 or later. You can download the latest
40 | version of Python [here](https://www.python.org/downloads/).
41 | I personally use [pyenv](https://github.com/pyenv/pyenv) to manage multiple
42 | python versions on my OSX development machine.
43 |
44 | In addition to python 3.5 or later, some parts of the tutorial will require
45 | the third party packages [requests](http://docs.python-requests.org/en/master/)
46 | and [aiohttp](http://aiohttp.readthedocs.io/en/stable/). These can be installed
47 | by the commands `pip install requests` and `pip install aiohttp`.
48 |
49 | I will generally write example code in short modules and then explore using
50 | them with `python -i module.py`. This will run the module and then continue
51 | in interactive mode where any functions or classes defined in the module
52 | are accessible. These short modules can be found in the [examples](examples)
53 | area of this repository.
54 |
55 | ## Slides as Presented
56 |
57 | [PDF format slides](conference_slides.pdf)
58 |
59 | ## Outline
60 |
61 | *Note*: The links in this outline lead to either my own notes for each
62 | section or an unfinished draft of the tutorial in written and expanded form.
63 |
64 | 1. [Warmup: Animals in the Cloud](warmup.md)
65 | 1. [Cooking with Concurrency](cooking.md)
66 | 1. [Understanding python Coroutines](coroutines.md)
67 | 1. [Getting to Know asyncio](asyncio.md)
68 | 1. [Animals and aiohttp](animals.md)
69 | 1. [Server Side Animals](webserver.md)
70 | 1. [Publish and Subscribe](pubsub.md)
71 |
--------------------------------------------------------------------------------
/conference_slides.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appeltel/CoroutineTutorial/6eb4608fe24deb7b9f426ef06e5c92f8a5f34613/conference_slides.pdf
--------------------------------------------------------------------------------
/cooking.md:
--------------------------------------------------------------------------------
1 | # Cooking with Concurrency
2 |
3 | Working with coroutines is a lot like using recipes in cooking, so to explain
4 | what concurrency is, here are a few simple recipes that I use to make a
5 | quick dinner for the family after work:
6 |
7 | ### Salmon filets with orange-ginger dressing
8 |
9 |
10 | |
11 |
12 |
13 | - Preheat oven to 350 degrees F
14 | - Arrange salmon filets on cooking sheet
15 | - Slather each filet with 2 tbsp. of orange-ginger dressing
16 | - Bake salmon in oven for 18 minutes
17 |
18 | |
19 |
20 |
21 | ### Rice pilaf from a box
22 |
23 |
24 | |
25 |
26 |
27 | - Put 1 3/4 cup of water and 2 tbsp. butter in 2 qt pot
28 | - Bring pot to a boil
29 | - Stir in spice package and rice pilaf, cover, set to low
30 | - Let simmer for 20-25 minutes
31 | - Fluff, let stand for 5 minutes
32 |
33 | |
34 |
35 |
36 | ### Steam in bag green beans
37 |
38 |
39 | |
40 |
41 |
42 | - Poke holes in bag, put on microwave-safe plate
43 | - Microwave for 5 minutes
44 |
45 | |
46 |
47 |
48 | ### Dinner!
49 |
50 | 
51 |
52 | This whole dinner takes about 30 minutes to make. Most of that time is spent
53 | just waiting for things to finish, and when I get one recipe into a state
54 | where I am waiting on the oven to heat up, or water to boil, I switch to
55 | another recipe and work on that for a while. Note that I never actually
56 | do two things at the same time. I am only ever engaged in one thing at a
57 | time, but I can handle multiple recipes going on at the same time.
58 |
59 | With that in mind, here are some definitions:
60 |
61 | * **Parallelism**: Doing multiple things at the same time.
62 | * **Concurrency**: Dealing with multiple things going on at the same time.
63 |
64 | Cooking this simple meal for the family in just 30 minutes requires me to
65 | use concurrency. I have to deal with multiple recipes being executed
66 | at the same time, i.e. *concurrently*. If I was only capable of working
67 | on a single recipe at a time, the dinner would take over an hour to prepare.
68 |
69 | If I had a recipe that contained a lot of actual work, perhaps chopping
70 | lots of different vegetables, then the speed at which I could finish the
71 | dinner would be limited by my ability to only do one thing at a time. I
72 | could in this case get a family member to help me, so that we would
73 | both be chopping at the same time - and this would be an example of
74 | *parallelism*.
75 |
76 | ## Dinner in Python
77 |
78 | Now reimagine this dinner as a python module:
79 |
80 | ```python
81 | from time import sleep
82 |
83 | from kitchen import (
84 | oven, stovetop, microwave,
85 | pot, baking_sheet, plate,
86 | salmon, orange_dressing, rice_box, water, butter, green_beans
87 | )
88 |
89 | def cook_salmon():
90 | oven.preheat(350)
91 | baking_sheet.place(salmon)
92 | for filet in salmon:
93 | filet.slater(ginger_dressing, amount=2)
94 | oven.insert(baking_sheet)
95 | sleep(18 * 60)
96 | return oven.extract_all()
97 |
98 | def cook_rice():
99 | pot.insert(water, amount=1.75)
100 | pot.inster(butter, amount=2)
101 | stovetop.add(pot)
102 | stovetop.set_burner(5)
103 | pot.wait_for_boil()
104 | pot.insert(box_rice)
105 | sleep(22*60)
106 | stovetop.set_burner(0)
107 | pot.fluff_contents()
108 | sleep(5*60)
109 | return pot.extract_all()
110 |
111 | def cook_beans():
112 | grean_beans.poke()
113 | microwave.insert(green_beans)
114 | microwave.cook(5*60, power=10)
115 | return microwave.extract_all()
116 |
117 | def make_salmon_dinner():
118 | meat = cook_fish()
119 | starch = cook_rice()
120 | veggie = cook_beans()
121 |
122 | return (meat, starch, veggie)
123 | ```
124 |
125 | The nice thing about this module is that the code is structured for reuse.
126 | If I wanted to make a similar dinner with chicken I could use the
127 | `cook_beans` and `cook_rice` functions, and only have to add another
128 | function to cook the chicken.
129 |
130 | Unfortunately, this will not execute concurrently. The `make_salmon_dinner`
131 | function will first call `cook_salmon` and wait for that to complete before
132 | moving on to `cook_rice`. That is a lot of wasted time!
133 |
134 | What I want is to execute all the recipes like a human would execute them.
135 | When the `cook_fish` function got to a place where it was simply waiting on
136 | the oven or sleeping, it would somehow release control and allow
137 | `cook_rice` to execute for a while, and then somehow come back to `cook_fish`
138 | where it left off.
139 |
140 | But this is not how functions work. A python function is called, executes, and
141 | then returns. You cannot just stop it in the middle and come back to it later.
142 | This is where python coroutines come in. As we will see in the next few
143 | sections, a coroutine is executed by a special scheduler and has the ability
144 | to release control back to the scheduler, and then continue where it left off.
145 | If `cook_fish`, `cook_beans`, and `cook_rice` were coroutines rather than
146 | functions, we could tell a scheduler to run them all concurrently and get
147 | the dinner done in a reasonable amount of time.
148 |
149 | While some applications require a lot of actual work to be done by the CPU,
150 | and therefore need parallelism to be executed more quickly, many others
151 | spend a lot of time waiting on network IO to complete. These sort of
152 | applications are very much like cooking dinner. There is lots of waiting
153 | around, and structuring the work with coroutines allows one to execute
154 | it all concurrently while preserving the modularity that you get by splitting
155 | the task into functions.
156 |
157 | ## More Gratuitous Cooking Metaphors
158 |
159 | There are a number of concepts and approaches to concurrency and parallelism,
160 | and to some degree they can all be described through various cooking
161 | metaphors. While this tutorial is really just focused on coroutines, it is
162 | worth at least mentioning some of the major ones.
163 |
164 | ### Callbacks
165 |
166 | If you have ever programmed in Javascript, you have likely encountered
167 | callbacks. Callbacks allow a scheduler (the cook) to execute some instructions
168 | after something occurs. For example, we could attach a callback function
169 | to the event that the oven is done preheating. Once this is done, the
170 | specified callback would be executed. The `cook_salmon` recipe could be
171 | broken up to use callbacks as follows:
172 |
173 | ```python
174 | def cook_salmon():
175 | oven.preheat(350, callback=cook_salmon_cb1)
176 | return
177 |
178 | def cook_salmon_cb1():
179 | baking_sheet.place(salmon)
180 | for filet in salmon:
181 | filet.slater(ginger_dressing, amount=2)
182 | oven.insert(baking_sheet)
183 | set_callback_timer(18 * 60, callback=cook_salmon_cb2)
184 | return
185 |
186 | def cook_salmon_cb2():
187 | return oven.extract_all()
188 | ```
189 |
190 | Here, each function returns when there is a waiting period, and the scheduler
191 | will just call the next function in the chain when the callback condition
192 | is satisfied. In the meantime, other work can get done. We could call
193 | `cook_salmon()` which would get the oven going and immediately return. Then
194 | we can call the next function for another recipe.
195 |
196 | The annoying thing about this is that the recipe was broken into a
197 | bunch of small functions and so it is harder to read. In other languages
198 | it is a somewhat common practice to define the callback function where it
199 | is passed as an argument as an anonymous function (lambda). This is generally
200 | not possible in python as anonymous functions are restricted, but if you could
201 | do it, then it might look like this:
202 |
203 |
204 | ```python
205 | def cook_salmon():
206 | oven.preheat(350, callback=lambda : (
207 | baking_sheet.place(salmon)
208 | for filet in salmon:
209 | filet.slater(ginger_dressing, amount=2)
210 | oven.insert(baking_sheet)
211 | set_callback_timer(18 * 60, callback=lambda: (
212 | return oven.extract_all()
213 | )
214 | )
215 | ```
216 |
217 | If there is a long chain of callbacks, then you can get a sort of cascading
218 | pattern of anonymous functions, sometimes referred to as "callback hell".
219 | This is also harder to read than a simple function.
220 |
221 | ### Coroutines
222 |
223 | A coroutine executes like a function, except it has places where it will
224 | yield control back to a scheduler when it is waiting on something, such as
225 | the oven heating up. When it yields, the scheduler (cook) is free to run
226 | another coroutine concurrently, and when that one yields it can continue
227 | to the first.
228 |
229 | Here is the `cook_salmon` recipe as a python coroutine:
230 |
231 | ```python
232 | async def cook_salmon():
233 | await oven.preheat(350)
234 | baking_sheet.place(salmon)
235 | for filet in salmon:
236 | filet.slater(ginger_dressing, amount=2)
237 | oven.insert(baking_sheet)
238 | await asyncio.sleep(18 * 60)
239 | return oven.extract_all()
240 | ```
241 |
242 | This is very nice as it looks much like a normal function, you can easily
243 | see how the recipe progresses. When it gets to a special `await` expression,
244 | it will yield control back to the scheduler to allow other recipes to run
245 | while the oven heats or the salmon bakes. How exactly this works will be
246 | described in detail in the next section.
247 |
248 | ### Threads
249 |
250 | Threads offer a way to run multiple functions concurrently without even
251 | changing the functions. A thread a separate unit of execution known to the
252 | operating system kernel, which will schedule each thread to run without
253 | you having to do anything. Python has a nice `threading` module in the
254 | standard library that makes this easy:
255 |
256 | ```python
257 | import threading
258 |
259 | salmon_thread = threading.Thread(target=cook_salmon)
260 | rice_thread = threading.Thread(target=cook_rice)
261 | beans_thread = threading.Thread(target=cook_beans)
262 |
263 | salmon_thread.start()
264 | rice_thread.start()
265 | beans_thread.start()
266 |
267 | salmon_thread.join()
268 | rice_thread.join()
269 | beans_thread.join()
270 | ```
271 |
272 | The call to `start()` will make each thread get scheduled for execution,
273 | and the `join()` will wait until the thread is done. So in this case all three
274 | recipes will run concurrently, and the code block will end once they are all
275 | finished. Unfortunately this will not return your dinner to you, the return
276 | value of the function called in the thread is lost, so the garbage collector
277 | will eat your dinner. To keep your dinner, you would need to rewrite the
278 | functions to put the dinner somewhere safe and shared, like a
279 | [Queue](https://docs.python.org/3.6/library/queue.html) object.
280 |
281 | The key thing that must be mentioned here is that threads are a form of
282 | preemptive multitasking. This means that the operating system kernel
283 | does not care exactly what you are doing in one thread, when it decides
284 | to switch to a different thread. You might be right in the middle of
285 | putting dressing on the salmon when the OS yells at you to get back to
286 | work on the beans. When writing functions to be executed as threads, you
287 | have no control over when the system switches between threads.
288 |
289 | Additionally, threads generally share resources. This means that if one
290 | thread is preheating the oven to 350, the OS could switch you to another
291 | that wants to preheat the oven to 425. The only way to avoid anarchy in the
292 | kitchen is to construct objects like
293 | [Semaphores](https://docs.python.org/2/library/threading.html#semaphore-objects)
294 | to control which thread is allowed to use the oven at any given time, forcing
295 | threads to wait until others are done using the oven.
296 |
297 | Finally, threads can in principle result in true parallelism. This means that
298 | if your system has multiple CPU cores, the OS kernel can schedule multiple
299 | threads to literally run at the same time. This is useful not just to avoid
300 | IO waits, but to get additional computational work done. In python, however,
301 | there is a Global Interpreter Lock (GIL) that prevents more than one thread
302 | from executing python instructions at the same time. There are ways that
303 | computational libraries like `numpy` avoid this restriction by releasing
304 | the GIL and performing computation with C-extensions, but that is the
305 | subject of a different tutorial.
306 |
307 | ### Multiprocessing
308 |
309 | Python also has a nice
310 | [multiprocessing](https://docs.python.org/3.6/library/multiprocessing.html)
311 | module to allow running functions in entirely separate processes, each
312 | with their own interpreter. This means that there is no GIL restriction,
313 | and multiple functions may be executed in parallel if there are multiple
314 | CPU cores available. It also means that no resources or memory are shared
315 | unless explicit constructs are used to communicate between processes.
316 |
317 | Think of this as running each recipe in its own kitchen. You have one
318 | kitchen for the salmon, one for the rice, and one for the beans. If one
319 | recipe was to call for the oven to be set to 350, and another to 425, then
320 | it does not matter. Unless an oven was deliberately set as a shared
321 | resource between the processes, then each kitchen has its own completely
322 | separate oven.
323 |
324 | Much like in the kitchen metaphor, in reality multiprocessing is more
325 | expensive in terms of memory usage, as each process gets a complete
326 | python interpreter and a copy of the full program in its own private
327 | memory allocation.
328 |
--------------------------------------------------------------------------------
/coroutines.md:
--------------------------------------------------------------------------------
1 | # Coroutines Run by Hand
2 |
3 | One way to really understand coroutines well is to construct a simple
4 | scheduling system to run them. A while back Brett Cannon wrote a
5 | [great article](https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/)
6 | on this very approach. Here I will go over coroutines using a simpler
7 | task of just running a few coroutines "by hand".
8 |
9 | ### Just Functions to Start
10 |
11 | Before getting in to how coroutines work in python and how to write them,
12 | I want to look at some simple functions first, so here is a module that
13 | we can call `byhand.py` with a few regular python functions:
14 |
15 | ```python
16 | import time
17 |
18 | def square(x):
19 | print('Starting square with argument {}!'.format(x))
20 | time.sleep(3)
21 | print('Finishing square with argument {}!'.format(x))
22 | return x * x
23 |
24 | def cube(x):
25 | print('Starting cube with argument {}!'.format(x))
26 | time.sleep(3)
27 | y = square(x)
28 | print('Finishing cube with argument {}!'.format(x))
29 | return x * y
30 | ```
31 |
32 | These functions have been made artificially slow and are needlessly verbose
33 | for what they actually do, but you could imagine that the functions needed
34 | to fetch some information from a database somewhere in order to multiply
35 | numbers. We will start by just running them:
36 |
37 | ```
38 | $ python -i byhand.py
39 | >>> square(4)
40 | Starting square with argument 4!
41 | Finishing square with argument 4!
42 | 16
43 | >>> cube(3)
44 | Starting cube with argument 3!
45 | Starting square with argument 3!
46 | Finishing square with argument 3!
47 | Finishing cube with argument 3!
48 | 27
49 | ```
50 |
51 | Then to create a goal for this section, consider running through a loop of
52 | numbers and calculating the cube of each one:
53 |
54 | ```
55 | $ python -i byhand.py
56 | >>> for x in range(1, 4):
57 | ... cube(x)
58 | ...
59 | Starting cube with argument 1!
60 | Starting square with argument 1!
61 | Finishing square with argument 1!
62 | Finishing cube with argument 1!
63 | 1
64 | Starting cube with argument 2!
65 | Starting square with argument 2!
66 | Finishing square with argument 2!
67 | Finishing cube with argument 2!
68 | 8
69 | Starting cube with argument 3!
70 | Starting square with argument 3!
71 | Finishing square with argument 3!
72 | Finishing cube with argument 3!
73 | 27
74 | ```
75 |
76 | This is very slow, and most of that time was spent sleeping. As with the
77 | cooking example in the last section, what I would like is to be able to
78 | run the function over all of the numbers in the range concurrently.
79 |
80 | But before making coroutines, a few words about functions and how great they
81 | are. When I call `square`, it does not care what it was called by, or anything
82 | beyond what arguments it is called with. It behaves the same no matter
83 | if it was called by `cube` or called directly, and simply returns a value
84 | to whatever called it when it is done. Function calls in a program naturally
85 | form a "stack" that grows as functions call other functions, and shrinks as
86 | they finish and return to their callers. This makes sequential code generally
87 | easy to reason about, as one can focus on a function as an isolated unit
88 | independent of anything other than its arguments. Of course, things like
89 | global variables and side effects ruin this simplicity, but in many cases
90 | it serves to keep otherwise large and complex code comprehensible.
91 |
92 | It would be nice to retain as much simplicity as possible when moving
93 | from sequential code (functions) to concurrent code (coroutines or threads).
94 | Keeping things simple is, in my mind, the key reason to consider using
95 | coroutines. They make it easy to reason about exactly when in your code you
96 | might switch between different tasks.
97 |
98 | ### On To Coroutines
99 |
100 | A regular function in python is defined using the keyword `def`. When you
101 | call a function, you instantiate an instance of that function with the
102 | arguments that you supplied. This function instance goes on the stack, is
103 | executed, and then returns some result to you.
104 |
105 | A coroutine function in python is defined with `async def`. We will go ahead
106 | and modify our simple module to turn the `square` function into a coroutine
107 | function:
108 |
109 |
110 | ```python
111 | import time
112 |
113 | async def square(x):
114 | print('Starting square with argument {}!'.format(x))
115 | time.sleep(3)
116 | print('Finishing square with argument {}!'.format(x))
117 | return x * x
118 |
119 | def cube(x):
120 | print('Starting cube with argument {}!'.format(x))
121 | time.sleep(3)
122 | y = square(x)
123 | print('Finishing cube with argument {}!'.format(x))
124 | return x * y
125 | ```
126 |
127 | Now we will run this, call square, and see what happens!
128 |
129 | ```
130 | $ python -i byhand.py
131 | >>> square(2)
132 |
133 | ```
134 |
135 | That might look odd. If you were to actually run the code in the coroutine
136 | function, it should return the integer 4. But instead it is returning a
137 | "coroutine object". What about running `cube`?
138 |
139 | ```
140 | >>> cube(3)
141 | Starting cube with argument 3!
142 | Finishing cube with argument 3!
143 | Traceback (most recent call last):
144 | File "", line 1, in
145 | File "coro_by_hand.py", line 14, in cube
146 | return x * y
147 | TypeError: unsupported operand type(s) for *: 'int' and 'coroutine'
148 | ```
149 |
150 | This makes sense when you look at the code for cube. `y` is just the result
151 | of calling `square(x)`, which is again the funny "coroutine object".
152 | Python does not know how to multiply an integer by a coroutine object, so
153 | when the `cube` function tries to multiply `x * y` it raises a `TypeError`.
154 |
155 | Now I will quit and then explain what is going on:
156 |
157 | ```
158 | >>> quit()
159 | sys:1: RuntimeWarning: coroutine 'square' was never awaited
160 | ```
161 |
162 | This is also interesting. Upon exit, the system complains that this
163 | "coroutine object" produced by calling `square` was never "awaited".
164 |
165 | To understand what is happening, consider what happens when a regular function
166 | is called. An instance of function is created with the arguments, it is placed
167 | at the top of the stack, the instructions are executed, and then a value is
168 | returned. With a coroutine function, an instance of the function - a coroutine
169 | object - is created, but it is not immediately executed. This object is
170 | returned, and it is up to a scheduler to actually execute the coroutine.
171 | So how does one run a coroutine object?
172 |
173 | A coroutine object has a `send` method which can be called with a single
174 | argument. When `send` is called, the coroutine begins execution. In many cases
175 | the `send` method will be called with `None` as an argument, but this argument
176 | can in principle be used for a scheduler to send information to a running
177 | coroutine. So here goes:
178 |
179 | ```
180 | $ python -i byhand.py
181 | >>> coro = square(4)
182 | >>> coro.send(None)
183 | Starting square with argument 4!
184 | Finishing square with argument 4!
185 | Traceback (most recent call last):
186 | File "", line 1, in
187 | StopIteration: 16
188 | >>> quit()
189 | ```
190 |
191 | Calling `send` caused the coroutine to run, but this raised a `StopIteration`
192 | exception. The result of 16 was returned as the value of this exception. So
193 | whatever runs a coroutine by calling `send` will need to catch this type of
194 | exception and read the value to get the result. Notice that since we called
195 | `send`, there was no `RuntimeWarning` about the coroutine never being awaited.
196 |
197 | So far so good - not terribly useful yet - but at least it works. What about
198 | the `cube` function that calls `square`? How does a function run a coroutine?
199 | Technically it could create a coroutine object and call `send`, but this is not
200 | a good practice. Generally speaking, functions never call coroutines and run
201 | them - only coroutines can call and run other coroutines.
202 |
203 | The way that a coroutine effectively "calls" another coroutine in the sense
204 | that a function calls another function is with the `await` expression. When
205 | a coroutine `awaits` a coroutine object, it delegates execution to that object,
206 | so calls to the `send` method are passed on to the awaited coroutine until
207 | it finishes. The coroutine that performed the await catches the `StopIteration`
208 | exception and the value becomes the result of the expression. That is a lot to
209 | digest, so here is it implemented in our example module:
210 |
211 | ```python
212 | import time
213 |
214 | async def square(x):
215 | print('Starting square with argument {}!'.format(x))
216 | time.sleep(3)
217 | print('Finishing square with argument {}!'.format(x))
218 | return x * x
219 |
220 | async def cube(x):
221 | print('Starting cube with argument {}!'.format(x))
222 | time.sleep(3)
223 | y = await square(x)
224 | print('Finishing cube with argument {}!'.format(x))
225 | return x * y
226 | ```
227 |
228 | Now we can run the `cube` coroutine function:
229 |
230 | ```
231 | >>> coro = cube(3)
232 | >>> coro.send(None)
233 | Starting cube with argument 3!
234 | Starting square with argument 3!
235 | Finishing square with argument 3!
236 | Finishing cube with argument 3!
237 | Traceback (most recent call last):
238 | File "", line 1, in
239 | StopIteration: 27
240 | ```
241 |
242 | That worked! But ultimately this is not doing anything that we could not
243 | simply accomplish with the regular functions. What coroutines should be able
244 | to do is pause execution when there is some reason to wait on IO or sleep.
245 | So in the case of this example it would be useful for a coroutine to yield
246 | control back to us when it encountered a `time.sleep` call.
247 |
248 | Now `time.sleep` is a reuglar function. So clearly coroutines can call
249 | regular functions, but while that function is executing the coroutine is
250 | unable to yield control back to the caller or scheduler.
251 |
252 | ### Yielding to the Scheduler
253 |
254 | In order to the above example
255 | useful we will replace `time.sleep` with a special
256 | type of coroutine that will yield control back to the scheduler which called
257 | `send` to run the coroutine. This is not something that you will need to
258 | ever write in practice unless you are writing a framework for scheduling
259 | coroutines, but we will need one here in order to schedule them "by hand":
260 |
261 | ```python
262 | import types
263 |
264 | @types.coroutine
265 | def manual_sleep(n):
266 | yield "Please do not continue until {} seconds have elapsed.".format(n)
267 |
268 |
269 | async def square(x):
270 | print('Starting square with argument {}!'.format(x))
271 | await manual_sleep(3)
272 | print('Finishing square with argument {}!'.format(x))
273 | return x * x
274 |
275 | async def cube(x):
276 | print('Starting cube with argument {}!'.format(x))
277 | await manual_sleep(3)
278 | y = await square(x)
279 | print('Finishing cube with argument {}!'.format(x))
280 | return x * y
281 | ```
282 |
283 | The `manual_sleep` coroutine is a special generator-based coroutine, and
284 | for the purposes of this tutorial we do not need to describe how that works
285 | in its entirety. The important point is that it can yield back control
286 | to whatever is running the coroutine by calling `send` - which is a human in
287 | this case! So try running this version:
288 |
289 | ```
290 | $ python -i byhand.py
291 | >>> coro = cube(3)
292 | >>> coro.send(None)
293 | Starting cube with argument 3!
294 | 'Please do not continue until 3 seconds have elapsed.'
295 | ```
296 |
297 | The coroutine did not finish! It executed until it awaited the first
298 | `manual_sleep` which yielded control back to us. We are the scheduler here,
299 | so it sent us polite instructions to let the coroutine object "sleep" for at
300 | least three seconds before continuing its execution with another `send`.
301 | So now we can finish the execution by calling `send` and following instructions
302 | until we get a `StopIteration`:
303 |
304 | ```
305 | >>> coro.send(None)
306 | Starting square with argument 3!
307 | 'Please do not continue until 3 seconds have elapsed.'
308 | >>> coro.send(None)
309 | Finishing square with argument 3!
310 | Finishing cube with argument 3!
311 | Traceback (most recent call last):
312 | File "", line 1, in
313 | StopIteration: 27
314 | ```
315 |
316 | To see how this really helps, consider two coroutine objects, `cube(5)` and
317 | `cube(10)`. We (the human scheduler) are capable of getting one of the
318 | objects running, and then while one is waiting three seconds, starting the
319 | other one and running it concurrently. As the two coroutines yield back to
320 | the scheduler, we can continue their execution by calling `send`:
321 |
322 | ```
323 | >>> coro1 = cube(5)
324 | >>> coro2 = cube(10)
325 | >>> coro1.send(None)
326 | Starting cube with argument 5!
327 | 'Please do not continue until 3 seconds have elapsed.'
328 | >>> coro2.send(None)
329 | Starting cube with argument 10!
330 | 'Please do not continue until 3 seconds have elapsed.'
331 | >>> coro1.send(None)
332 | Starting square with argument 5!
333 | 'Please do not continue until 3 seconds have elapsed.'
334 | >>> coro2.send(None)
335 | Starting square with argument 10!
336 | 'Please do not continue until 3 seconds have elapsed.'
337 | >>> coro1.send(None)
338 | Finishing square with argument 5!
339 | Finishing cube with argument 5!
340 | Traceback (most recent call last):
341 | File "", line 1, in
342 | StopIteration: 125
343 | >>> coro2.send(None)
344 | Finishing square with argument 10!
345 | Finishing cube with argument 10!
346 | Traceback (most recent call last):
347 | File "", line 1, in
348 | StopIteration: 1000
349 | ```
350 |
351 | From this it should be clear how using coroutines is an example of
352 | cooperative rather than pre-emptive multitasking. The scheduler cannot
353 | tell a coroutine that it is time to stop and allow another to run, it
354 | has to wait until the coroutine willingly yields control back to the
355 | scheduler.
356 |
357 | To make this useful we need a framework to provide a scheduler that can
358 | run coroutines without human intervention and return to us the
359 | return value of each coroutine. It also will need a set of special
360 | coroutines to perform basic I/O and sleep functions and can communicate
361 | correctly with the scheduler.
362 |
363 | ### Using a Real Scheduler
364 |
365 | Python is a "batteries included" language, and ships with the `asyncio`
366 | library which provides such a framework. It has a scheduler to run
367 | coroutines, called an "event loop", and special coroutines needed to
368 | yield back to the event loop. For this example, we only need to use
369 | `asyncio.sleep`. Awaiting `asyncio.sleep(n)` with a number `n` will yield
370 | control to the asyncio event loop, and instruct it to wait at least `n`
371 | seconds before resuming the coroutine. In order to use this, we need to
372 | first slightly rewrite the example module to use `asyncio.sleep` rather than
373 | our `manual_sleep` coroutine function:
374 |
375 | ```python
376 | import asyncio
377 |
378 | async def square(x):
379 | print('Starting square with argument {}!'.format(x))
380 | await asyncio.sleep(3)
381 | print('Finishing square with argument {}!'.format(x))
382 | return x * x
383 |
384 | async def cube(x):
385 | print('Starting cube with argument {}!'.format(x))
386 | await asyncio.sleep(3)
387 | y = await square(x)
388 | print('Finishing cube with argument {}!'.format(x))
389 | return x * y
390 | ```
391 |
392 | Now run this module interactively and schedule a `cube(5)` coroutine
393 | to be executed:
394 |
395 | ```
396 | $ python -i byhand.py
397 | >>> loop = asyncio.get_event_loop()
398 | >>> result = loop.run_until_complete(cube(5))
399 | Starting cube with argument 5!
400 | Starting square with argument 5!
401 | Finishing square with argument 5!
402 | Finishing cube with argument 5!
403 | >>> result
404 | 125
405 | ```
406 |
407 | Great! But how does one use this to execute coroutines concurrently? For this
408 | asyncio provides an `asyncio.gather` coroutine function. This special
409 | coroutine function takes any number of coroutine objects as arugments, and
410 | tells the event loop to execute them all concurrently. When all of the
411 | coroutines have finished running, it returns all their results in a list.
412 | Here goes:
413 |
414 | ```
415 | $ python -i byhand.py
416 | >>> loop = asyncio.get_event_loop()
417 | >>> coro = asyncio.gather(cube(3), cube(4), cube(5))
418 | >>> results = loop.run_until_complete(coro)
419 | Starting cube with argument 4!
420 | Starting cube with argument 3!
421 | Starting cube with argument 5!
422 | Starting square with argument 4!
423 | Starting square with argument 3!
424 | Starting square with argument 5!
425 | Finishing square with argument 4!
426 | Finishing cube with argument 4!
427 | Finishing square with argument 3!
428 | Finishing cube with argument 3!
429 | Finishing square with argument 5!
430 | Finishing cube with argument 5!
431 | >>> results
432 | [27, 64, 125]
433 | ```
434 |
435 | Notice that the order in which the coroutines are run is not deterministic. You
436 | can try running this example again and see that it may or may not occur in
437 | a different order. However, the list of results is returned in the order
438 | in which the coroutine objects were given to `asyncio.gather` as arguments.
439 |
440 | ### Schedulers, Schedulers, Schedulers
441 |
442 | A brief mention of twisted, curio, trio, uvloop
443 |
444 | ### A Bit More New Syntax
445 |
446 | Before progressing to the next section, where the real fun will begin with
447 | actual concurrent IO over an actual network, there is a bit more coroutine
448 | syntax to understand.
449 |
450 | First, consider a basic python for loop:
451 |
452 | ```python
453 | for item in container:
454 | do_stuff(item)
455 | do_more_stuff(item)
456 | ```
457 |
458 | In this example, the object `container` could be a lot of things. It could
459 | be a list, a dict, a generator, or even a string. You can define your own
460 | classes to adhere to the simple python
461 | [iterator protocol](https://docs.python.org/3/library/stdtypes.html#iterator-types)
462 | which can then be iterated over in for loops. The way this works is that a
463 | special `__iter__` method is called on the container to provide an iterator
464 | object, and then at the top of each pass through the loop, a special
465 | `__next__` method is called on the iterator which returns a value to be
466 | assigned to `item`. The point here is that this `__next__` method is a
467 | regular function - not a coroutine function.
468 |
469 | So imagine that we had a special `container` class that did not store its
470 | data locally. Instead, in order to fetch each item with `__next__` it had to
471 | make a network connection and get it from a remote database. This would
472 | be bad to do in a coroutine, as it is a regular function, and a regular
473 | function cannot yield control to the scheduler to run other coroutines
474 | while we are waiting for this item to be fetched.
475 |
476 | The solution to this problem is an
477 | [async for](https://docs.python.org/3/reference/compound_stmts.html#async-for)
478 | statement, which works much like a for statement, except that it calls slightly
479 | different special methods on the container which should result in a
480 | coroutine object that is awaited on to return the item used for each pass of
481 | the loop. Basically, this means that while the async for loop is doing
482 | its business to fetch the item from some remote database, it can yield
483 | back to the scheduler in order to allow other coroutines to run.
484 |
485 |
486 | ```python
487 | async for item in remote_container: # Here we might yield to the scheduler!
488 | do_stuff(item)
489 | do_more_stuff(item)
490 | ```
491 |
492 | The last new important piece of syntax involves
493 | [context managers](https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers)
494 | and the `with` statement. Consider an example of a hypothetical key-value
495 | database that uses a context manager to handle connections. To set a
496 | key in the database, your statement might look like this:
497 |
498 | ```python
499 | with database.connect('proto://db.somedomain.net') as db:
500 | db.set_key('cow', 'Moo!')
501 | ```
502 |
503 | What is going on behind the scenes is that the `database.connect` call returns
504 | a context manager, and the interpreter calls a special `__enter__` method
505 | that sets up the database connection (this involves waiting on the network)
506 | and returns an object that is assigned to `db` for us to use inside the with
507 | block to talk to the database.
508 |
509 | Then when the with block is completed, or even if it does not complete
510 | due to an exception, the context manager special `__exit__` method is called
511 | which can close the connection and perform any needed cleanup (more waiting
512 | on the network!).
513 |
514 | Unfortunately, if one was to use such a `with` statement in a coroutine
515 | that needed to make network connections on its enter and exit, the functions
516 | for entering and exiting would be unable to yield control back to the
517 | scheduler and allow other coroutines to run while waiting on the connection
518 | setup or teardown.
519 |
520 | To solve this, there is an `async with` statement which expects an
521 | asynchronous context manager that provides coroutines on enter and exit that
522 | are awaited on before entering or exiting the `async with` block. This allows
523 | a coroutine containing the `async with` statement to yeild control to the
524 | scheduler during this setup and allowing for other coroutines to run.
525 | An example might look like this:
526 |
527 | ```python
528 | async with database.connect('proto://db.somedomain.net') as db:
529 | await db.set_key('cow', 'Moo!')
530 | ```
531 |
532 | Here the coroutine is able to yeild to the scheduler when the network
533 | connection is being set up, when the data for the new key is being sent to
534 | the database, and when it cleans up.
535 |
536 | ### One Last Word on Awaiting
537 |
--------------------------------------------------------------------------------
/examples/animals.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | BASE_URL = 'https://ericappelt.com/animals/'
4 |
5 |
6 | def speak(animal, session):
7 |
8 | response = session.get('{}/{}'.format(BASE_URL, animal))
9 | response.raise_for_status()
10 | sound = response.text
11 |
12 | return 'The {} says "{}".'.format(animal, sound)
13 |
14 |
15 | def main():
16 | animals = ['cow', 'pig', 'chicken']
17 | session = requests.Session()
18 | for animal in animals:
19 | response = speak(animal, session)
20 | print(response)
21 |
22 |
23 | if __name__ == '__main__':
24 | main()
25 |
--------------------------------------------------------------------------------
/examples/animals_asyncio.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import aiohttp
4 |
5 | BASE_URL = 'https://ericappelt.com/animals/'
6 |
7 |
8 | async def speak(animal, session):
9 |
10 | async with session.get('{}/{}'.format(BASE_URL, animal)) as response:
11 | response.raise_for_status()
12 | sound = await response.text()
13 |
14 | return 'The {} says "{}".'.format(animal, sound)
15 |
16 |
17 | async def main():
18 | animals = ['cow', 'pig', 'chicken']
19 | coroutines = []
20 | async with aiohttp.ClientSession() as session:
21 | for animal in animals:
22 | coro = speak(animal, session)
23 | coroutines.append(coro)
24 |
25 | responses = await asyncio.gather(*coroutines)
26 |
27 | for line in responses:
28 | print(line)
29 |
30 |
31 | if __name__ == '__main__':
32 | loop = asyncio.get_event_loop()
33 | loop.run_until_complete(main())
34 | loop.close()
35 |
--------------------------------------------------------------------------------
/examples/byhand_asyncio.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | async def square(x):
4 | print('Starting square with argument {}!'.format(x))
5 | await asyncio.sleep(3)
6 | print('Finishing square with argument {}!'.format(x))
7 | return x * x
8 |
9 | async def cube(x):
10 | print('Starting cube with argument {}!'.format(x))
11 | await asyncio.sleep(3)
12 | y = await square(x)
13 | print('Finishing cube with argument {}!'.format(x))
14 | return x * y
15 |
--------------------------------------------------------------------------------
/examples/byhand_coroutines_manual_sleep.py:
--------------------------------------------------------------------------------
1 | import types
2 |
3 | @types.coroutine
4 | def manual_sleep(n):
5 | yield "Please do not continue until {} seconds have elapsed.".format(n)
6 |
7 |
8 | async def square(x):
9 | print('Starting square with argument {}!'.format(x))
10 | await manual_sleep(3)
11 | print('Finishing square with argument {}!'.format(x))
12 | return x * x
13 |
14 | async def cube(x):
15 | print('Starting cube with argument {}!'.format(x))
16 | await manual_sleep(3)
17 | y = await square(x)
18 | print('Finishing cube with argument {}!'.format(x))
19 | return x * y
20 |
--------------------------------------------------------------------------------
/examples/byhand_coroutines_time_sleep.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | async def square(x):
4 | print('Starting square with argument {}!'.format(x))
5 | time.sleep(3)
6 | print('Finishing square with argument {}!'.format(x))
7 | return x * x
8 |
9 | async def cube(x):
10 | print('Starting cube with argument {}!'.format(x))
11 | time.sleep(3)
12 | y = await square(x)
13 | print('Finishing cube with argument {}!'.format(x))
14 | return x * y
15 |
--------------------------------------------------------------------------------
/examples/byhand_functions.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | def square(x):
4 | print('Starting square with argument {}!'.format(x))
5 | time.sleep(3)
6 | print('Finishing square with argument {}!'.format(x))
7 | return x * x
8 |
9 | def cube(x):
10 | print('Starting cube with argument {}!'.format(x))
11 | time.sleep(3)
12 | y = square(x)
13 | print('Finishing cube with argument {}!'.format(x))
14 | return x * y
15 |
--------------------------------------------------------------------------------
/examples/gettoknow_exceptions.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | async def square(x):
4 | print('Starting square with argument {}!'.format(x))
5 | if x == 7:
6 | raise ValueError("Can't square 7 - I'm not very good at this :(")
7 | await asyncio.sleep(3)
8 | print('Finishing square with argument {}!'.format(x))
9 | return x * x
10 |
11 | async def list_squares(n):
12 | coros = []
13 | for idx in range(n):
14 | coros.append(square(idx))
15 |
16 | results = await asyncio.gather(*coros, return_exceptions=True)
17 | return results
18 |
--------------------------------------------------------------------------------
/examples/gettoknow_executor.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import time
3 |
4 | async def square(x):
5 | print('Starting square with argument {}!'.format(x))
6 | if x == 7:
7 | raise ValueError("Can't square 7 - I'm not very good at this :(")
8 | await asyncio.sleep(3)
9 | print('Finishing square with argument {}!'.format(x))
10 | return x * x
11 |
12 | def blocking_square(x):
13 | print('Starting blocking square with argument {}!'.format(x))
14 | time.sleep(3)
15 | print('Finishing blocking square with argument {}!'.format(x))
16 | return x * x
17 |
18 | async def list_squares(n):
19 | loop = asyncio.get_event_loop()
20 | coros = []
21 | for idx in range(n):
22 | if idx == 7:
23 | coro = loop.run_in_executor(None, blocking_square, idx)
24 | coros.append(coro)
25 | else:
26 | coros.append(square(idx))
27 |
28 | results = await asyncio.gather(*coros, return_exceptions=True)
29 | return results
30 |
--------------------------------------------------------------------------------
/examples/gettoknow_forever.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | async def stop_in(n):
4 | loop = asyncio.get_event_loop()
5 | while n > 0:
6 | print('Shutdown in {}...'.format(n))
7 | await asyncio.sleep(1)
8 | n = n - 1
9 |
10 | loop.stop()
11 |
--------------------------------------------------------------------------------
/examples/gettoknow_tasks.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | async def square(x):
4 | print('Starting square with argument {}!'.format(x))
5 | await asyncio.sleep(3)
6 | print('Finishing square with argument {}!'.format(x))
7 | return x * x
8 |
9 | async def launch_square(x):
10 | loop = asyncio.get_event_loop()
11 | task = loop.create_task(square(x))
12 | while not task.done():
13 | print('waiting for square({})...'.format(x))
14 | await asyncio.sleep(1)
15 |
16 | return task.result()
17 |
--------------------------------------------------------------------------------
/examples/pubsub_aiohttp.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from aiohttp import web
3 |
4 |
5 | class Hub():
6 |
7 | def __init__(self):
8 | self.subscriptions = set()
9 |
10 | def publish(self, message):
11 | for queue in self.subscriptions:
12 | queue.put_nowait(message)
13 |
14 |
15 | class Subscription():
16 |
17 | def __init__(self, hub):
18 | self.hub = hub
19 | self.queue = asyncio.Queue()
20 |
21 | def __enter__(self):
22 | hub.subscriptions.add(self.queue)
23 | return self.queue
24 |
25 | def __exit__(self, type, value, traceback):
26 | hub.subscriptions.remove(self.queue)
27 |
28 |
29 | hub = Hub()
30 |
31 |
32 | async def sub(request):
33 | resp = web.StreamResponse()
34 | resp.headers['content-type'] = 'text/plain'
35 | resp.status_code = 200
36 | await resp.prepare(request)
37 | with Subscription(hub) as queue:
38 | while True:
39 | msg = await queue.get()
40 | resp.write(bytes(f'{msg}\r\n', 'utf-8'))
41 | return resp
42 |
43 |
44 | async def pub(request):
45 | msg = request.query.get('msg', '')
46 | hub.publish(msg)
47 | return web.Response(text='ok')
48 |
49 |
50 | if __name__ == '__main__':
51 | app = web.Application()
52 | app.router.add_get('/', sub)
53 | app.router.add_post('/', pub)
54 | web.run_app(app)
55 |
--------------------------------------------------------------------------------
/examples/webservice_animals_aiottp.py:
--------------------------------------------------------------------------------
1 | from asyncio import sleep
2 |
3 | from aiohttp import web
4 |
5 | FARM = {
6 | 'cow': 'Moo!',
7 | 'pig': 'Oink!',
8 | 'sheep': 'Baaa!',
9 | 'chicken': 'Cluck!',
10 | 'bird': 'Tweet!',
11 | 'duck': 'Quack!',
12 | 'dog': 'Woof!',
13 | 'cat': 'Meow!',
14 | 'frog': 'Ribbit!',
15 | 'horse': 'Neigh!',
16 | 'turkey': 'Gobble-Gobble!',
17 | 'rooster': 'Cock-a-Doodle-Doo!'
18 | }
19 |
20 |
21 | async def hello(request):
22 | msg = 'Welcome to the farm!'
23 | return web.Response(text=msg)
24 |
25 |
26 | async def speak(request):
27 | animal = request.match_info['name']
28 | if animal not in FARM:
29 | return web.Response(
30 | text='The animal {0} was not found.'.format(animal),
31 | status=404
32 | )
33 |
34 | await sleep(5)
35 | return web.Response(text=FARM[animal])
36 |
37 |
38 | app = web.Application()
39 | app.router.add_get('/animals', hello)
40 | resource = app.router.add_resource('/animals/{name}')
41 | resource.add_route('GET', speak)
42 |
43 | web.run_app(app)
44 |
--------------------------------------------------------------------------------
/media/beans.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appeltel/CoroutineTutorial/6eb4608fe24deb7b9f426ef06e5c92f8a5f34613/media/beans.jpg
--------------------------------------------------------------------------------
/media/dinner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appeltel/CoroutineTutorial/6eb4608fe24deb7b9f426ef06e5c92f8a5f34613/media/dinner.jpg
--------------------------------------------------------------------------------
/media/rice.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appeltel/CoroutineTutorial/6eb4608fe24deb7b9f426ef06e5c92f8a5f34613/media/rice.jpg
--------------------------------------------------------------------------------
/media/salmon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appeltel/CoroutineTutorial/6eb4608fe24deb7b9f426ef06e5c92f8a5f34613/media/salmon.jpg
--------------------------------------------------------------------------------