├── 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 | 19 |
salmon filets and dressing 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 |
20 | 21 | ### Rice pilaf from a box 22 | 23 | 24 | 25 | 34 |
box of rice pilaf 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 |
35 | 36 | ### Steam in bag green beans 37 | 38 | 39 | 40 | 46 |
steam in bag green beans 41 |
    42 |
  • Poke holes in bag, put on microwave-safe plate
  • 43 |
  • Microwave for 5 minutes
  • 44 |
45 |
47 | 48 | ### Dinner! 49 | 50 | ![Salmon Dinner](media/dinner.jpg "Salmon Dinner!") 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 --------------------------------------------------------------------------------