Skip to content

Commit 60bc2f9

Browse files
committed
feat: include async intro
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
1 parent 8b9daa8 commit 60bc2f9

File tree

3 files changed

+77
-0
lines changed

3 files changed

+77
-0
lines changed

content/week11/concepts.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,35 @@ Then you may need "shared" memory. This might be the default for
148148
threading/async, but is required for multiprocessing and subinterpreters. This
149149
is not an option if you are running on multiple machines; then you must transfer
150150
serialized objects instead. See the examples in the distributed section.
151+
152+
## Event loop
153+
154+
One common design for a reactive program is an event loop, where the program is
155+
designed around a central loop, and it reacts to input. This is common with
156+
async programming, but is also used by things like older GUI toolkits without
157+
async, as well. Let's try creating our own from scratch using generators:
158+
159+
```{literalinclude} conceptsexample/eventloop.py
160+
161+
```
162+
163+
In this example, we start with tasks, and loop over them. Each task returns an
164+
estimate of how long it will take. If you were to use one task within another
165+
task, you would need `yield from` for the inner task. The loop waits for the
166+
shortest task to be ready, then tries again. It's basic, but the idea is there.
167+
168+
We are using the generator system in Python (asyncio was built originally using
169+
it), but we could have implemented it with the async special methods instead; it
170+
would have been more verbose (since those weren't really designed to be hand
171+
implemented simply), but is quite doable.
172+
173+
Let's try the same thing with asyncio:
174+
175+
```{literalinclude} conceptsexample/asyncloop.py
176+
177+
```
178+
179+
We don't sort the output here, but otherwise, it runs about the same, and takes
180+
the same total amount of time. The difference here is we using a pre-existing
181+
awaitable (sleep), so we have to use `await`, which is really just `yield from`
182+
but using the async-named special methods.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import asyncio
2+
3+
4+
async def sleep(t):
5+
await asyncio.sleep(t)
6+
return f"Sleep {t} over"
7+
8+
9+
async def main():
10+
results = await asyncio.gather(sleep(3), sleep(2), sleep(1), sleep(4))
11+
print(results)
12+
13+
14+
asyncio.run(main())
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import time
2+
3+
4+
class NotReady(float):
5+
pass
6+
7+
8+
def event_loop(tasks):
9+
while tasks: # Stops when all tasks are done
10+
waits = []
11+
for task in tasks:
12+
try:
13+
res = task.send(None) # async function runs here
14+
if isinstance(res, NotReady):
15+
waits.append(res) # Build up all the requested waits
16+
else:
17+
yield res # Produce result
18+
except StopIteration:
19+
tasks.remove(task) # Task done, remove from tasks
20+
if waits:
21+
time.sleep(min(waits)) # Wait for the shortest requested wait
22+
23+
24+
def sleep(t):
25+
endtime = time.time() + t
26+
while time.time() < endtime:
27+
yield NotReady(endtime - time.time())
28+
yield f"Sleep {t} over"
29+
30+
31+
print(*event_loop([sleep(3), sleep(2), sleep(1), sleep(4)]), sep="\n")

0 commit comments

Comments
 (0)