PART 3 · ADVANCED PYTHON · CHAPTER 13
Concurrency: threads, processes, asyncio
FreeLesson 37 of 78
Waiting on the network and crunching numbers need different tools. Pick by what your code spends its time doing.
import time
from concurrent.futures import ThreadPoolExecutor
def fetch(page): # stands in for an API call
time.sleep(0.3) # waiting on the network
return f"page {page}: 100 rows"
start = time.perf_counter()
results = [fetch(p) for p in range(1, 6)]
print(f"one by one: {time.perf_counter() - start:.1f}s")
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=5) as pool:
results = list(pool.map(fetch, range(1, 6)))
print(f"5 threads: {time.perf_counter() - start:.1f}s")
print(results[0], "...", results[-1])Output
one by one: 1.5s 5 threads: 0.3s page 1: 100 rows ... page 5: 100 rows
The times are what this run measured when the book was built. Yours will differ.
import asyncio, time
async def fetch(page):
await asyncio.sleep(0.3) # non-blocking wait
return page * 100
async def main():
start = time.perf_counter()
jobs = (fetch(p) for p in range(1, 6))
rows = await asyncio.gather(*jobs)
took = time.perf_counter() - start
print(rows, f"{took:.1f}s")
asyncio.run(main())Output
[100, 200, 300, 400, 500] 0.3s
The times are what this run measured when the book was built. Yours will differ.
from concurrent.futures import ProcessPoolExecutor
def checksum(chunk): # CPU-bound work
return sum(x * x % 7 for x in chunk)
if __name__ == "__main__": # needed for processes
step = 200_000
chunks = [range(i, i + step)
for i in range(0, 800_000, step)]
with ProcessPoolExecutor(max_workers=2) as pool:
print(list(pool.map(checksum, chunks)))Output
[399999, 400002, 399996, 400002]
| Threads | Processes | asyncio |
|---|---|---|
| Best for I/O: API calls, S3 downloads, DB queries | CPU: parsing, hashing, compression, heavy transforms | Thousands of concurrent I/O calls |
| GIL effect Only one thread runs Python bytecode at a time | Each process has its own interpreter and GIL | Single thread, so no GIL contention |
| Cost Low | High: separate memory, data must be pickled | Lowest, but every library call must be async |
| DE example Pull 50 API pages in parallel | Parse 200 large files on 8 cores | aiohttp crawler, async DB drivers |
Practice before you move on
- Change the thread pool in the first example to two workers and compare the elapsed time.
- Write one line on which of the three tools you would use to parse many large files, and why.