Skip to content

Commit 9b754a4

Browse files
authored
Minor suggestions for parallel module (#81)
* Update practices.md Updated most likely misinterpretation on part of caller. * minor verbiage changes * Added old asyncpi.py as asyncpi_thread.py
1 parent bef2287 commit 9b754a4

File tree

4 files changed

+83
-21
lines changed

4 files changed

+83
-21
lines changed

content/week01/practices.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ get_rect_area(x1, y1, x2, y2)
272272
```
273273

274274
Someone calling the function could easily make a mistake:
275-
`get_rect_area(x1, x2, y1, y1)` for example. However, if you bundle this:
275+
`get_rect_area(x1, x2, y1, y2)` for example. However, if you bundle this:
276276

277277
```python
278278
def get_rect_area(point_1, point_2): ... # does stuff

content/week11/piexample/asyncpi.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,23 @@ async def timer():
1313
print(f"Took {time.monotonic() - start:.3}s to run")
1414

1515

16-
def pi_each(trials: int) -> None:
16+
async def pi_async(trials: int) -> float:
1717
Ncirc = 0
1818
rand = random.Random()
1919

20-
for _ in range(trials):
20+
for trial in range(trials):
2121
x = rand.uniform(-1, 1)
2222
y = rand.uniform(-1, 1)
2323

2424
if x * x + y * y <= 1:
2525
Ncirc += 1
2626

27-
return 4.0 * (Ncirc / trials)
28-
27+
# yield to event loop every 1000 iterations
28+
# This allows other asyncs to do their job
29+
if trial % 1000 == 0:
30+
await asyncio.sleep(0)
2931

30-
async def pi_async(trials: int):
31-
return await asyncio.to_thread(pi_each, trials)
32+
return 4.0 * (Ncirc / trials)
3233

3334

3435
@timer()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import contextlib
2+
import random
3+
import statistics
4+
import threading
5+
import time
6+
import asyncio
7+
8+
9+
@contextlib.asynccontextmanager
10+
async def timer():
11+
start = time.monotonic()
12+
yield
13+
print(f"Took {time.monotonic() - start:.3}s to run")
14+
15+
16+
def pi_each(trials: int) -> None:
17+
Ncirc = 0
18+
rand = random.Random()
19+
20+
for _ in range(trials):
21+
x = rand.uniform(-1, 1)
22+
y = rand.uniform(-1, 1)
23+
24+
if x * x + y * y <= 1:
25+
Ncirc += 1
26+
27+
return 4.0 * (Ncirc / trials)
28+
29+
30+
async def pi_async(trials: int):
31+
return await asyncio.to_thread(pi_each, trials)
32+
33+
34+
@timer()
35+
async def pi_all(trials: int, threads: int) -> float:
36+
async with asyncio.TaskGroup() as tg:
37+
tasks = [tg.create_task(pi_async(trials // threads)) for _ in range(threads)]
38+
return statistics.mean(t.result() for t in tasks)
39+
40+
41+
def pi(trials: int, threads: int) -> float:
42+
return asyncio.run(pi_all(trials, threads))
43+
44+
45+
print(f"{pi(10_000_000, 10)=}")

content/week11/threading.md

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ memory. This is much heaver weight than threading, but can be used effectively
4141
sometimes.
4242

4343
Recently, there have been two major attempts to improve access to multiple cores
44-
in Python. Python 3.12 added a subinterpeters each with their own GIL; two pure
44+
in Python. Python 3.12 added subinterpeters each with their own GIL; two pure
4545
Python ways to access these are being added in Python 3.14 (previously there was
4646
only a C API and third-party wrappers). Compiled extensions have to opt-into
4747
supporting multiple interpreters.
@@ -442,7 +442,7 @@ something like a notebook.
442442
Here's our π example. Since we don't have to communicate anything other than a
443443
integer, it's trivial and reasonably performant, minus the start up time:
444444

445-
```{literalinclude} piexample/threadexec.py
445+
```{literalinclude} piexample/procexec.py
446446
:linenos:
447447
:lineno-match: true
448448
:lines: 15-
@@ -469,18 +469,15 @@ also making the context manager async:
469469
:linenos:
470470
```
471471

472-
Since the actual multithreading above comes from moving a function into threads,
473-
it is identical to the threading examples when it comes to performance (same-ish
474-
on normal Python, faster on free-threaded). The `async` part is about the
475-
control flow. Outside of the `to_thread` part, we don't have to worry about
476-
normal thread issues, like data races, thread safety, etc, as it's just oddly
477-
written single threaded code. Every place you see `await`, that's where code
478-
pauses, gives up control and lets the event loop (which is created by
479-
`asyncio.run`, there are third party ones too) take control and "unpause" some
480-
other waiting `async` function if it's ready. It's great for things that take
481-
time, like IO. This is not as commonly used for threaded code like we've done,
482-
but more for "reactive" programs that do something based on external input
483-
(GUIs, networking, etc).
472+
Every place you see `await`, that's where code pauses, gives up control and lets
473+
the event loop (which is created by `asyncio.run`, there are third party ones
474+
too) take control and "unpause" some other waiting `async` function if it's
475+
ready.
476+
477+
You will notice no performance improvement over the single-threaded version of
478+
the code, since the asyncio event loop runs on the main thread, and relies on
479+
the async function to give up control so that other async functions can proceed,
480+
like we've done using `asyncio.sleep()`.
484481

485482
Notice how we didn't need a special `queue` like in some of the other examples.
486483
We could just create and loop over a normal list filled with tasks.
@@ -489,3 +486,22 @@ Also notice that these "async functions" are called and create the awaitable
489486
object, so we didn't need any odd `(f, args)` syntax when making them, just the
490487
normal `f(args)`. Every object you create that is awaitable should eventually be
491488
awaited, Python will show a warning otherwise.
489+
490+
`async` is great for processing that takes time but shouldn't hog up all the
491+
CPU. It is mostly used for "reactive" programs that do something based on
492+
external input (GUIs, networking, etc).
493+
494+
It is also possible to run `async` code in a thread by awaiting on
495+
`asyncio.to_thread(async_function, *args)`.
496+
497+
```{literalinclude} piexample/asyncpi_thread.py
498+
:linenos:
499+
```
500+
501+
Since the actual multithreading above comes from moving a function into threads,
502+
it is identical to the threading examples when it comes to performance (same-ish
503+
on normal Python, faster on free-threaded).
504+
505+
Outside of the `to_thread` part, we don't have to worry about normal thread
506+
issues, like data races, thread safety, etc, as it's just oddly written single
507+
threaded code.

0 commit comments

Comments
 (0)