Cohort 4Register

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.

THREADS FOR I/O-BOUND WORKPYTHON 3.12
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.

ASYNCIO: ONE THREAD, MANY WAITSPYTHON 3.12
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.

PROCESSES FOR CPU-BOUND WORKPYTHON 3.12
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]
ThreadsProcessesasyncio
Best for I/O: API calls, S3 downloads, DB queriesCPU: parsing, hashing, compression, heavy transformsThousands of concurrent I/O calls
GIL effect Only one thread runs Python bytecode at a timeEach process has its own interpreter and GILSingle thread, so no GIL contention
Cost LowHigh: separate memory, data must be pickledLowest, but every library call must be async
DE example Pull 50 API pages in parallelParse 200 large files on 8 coresaiohttp crawler, async DB drivers

Practice before you move on

  1. Change the thread pool in the first example to two workers and compare the elapsed time.
  2. Write one line on which of the three tools you would use to parse many large files, and why.