0% complete

Testing, debugging, and concurrency · Lesson 3 of 3

Threads, processes, and asynchronous I/O

Choose threads, processes, or asyncio according to the workload and library support.

Lesson

1. Concept and mental model

Concurrency helps a program make progress on multiple tasks, but the right technique depends on the work. Threads are often useful for I/O-bound tasks that spend time waiting on network or disk operations. Processes can use multiple CPU cores for CPU-bound Python work, at the cost of process startup and data-transfer overhead. asyncio supports many concurrent I/O operations when the libraries involved provide nonblocking asynchronous APIs.

2. How it works

Concurrency introduces new failure modes: shared-state races, ordering assumptions, cancellation, partial completion, and resource exhaustion. Limit the number of outstanding tasks; launching an unbounded task for every input can overwhelm a service or exhaust memory. Use timeouts and explicit cancellation behavior. Prefer passing data between workers over mutating the same global structure from multiple threads.

3. Use it safely and effectively

The global interpreter lock affects execution of Python bytecode in common CPython builds, so adding threads does not generally speed up CPU-heavy pure-Python work. Native libraries may release it, and runtime details differ. Measure the real workload and keep a sequential implementation as a baseline. Practice with a small set of network requests using bounded concurrency, then verify failures and results are associated with the correct input.

4. Syntax reference

import asyncio

async def task(name: str) -> str:
    await asyncio.sleep(0.01)
    return f'done: {name}'

async def main() -> None:
    results = await asyncio.gather(task('A'), task('B'))
    print(results)

asyncio.run(main())

5. Guided walkthrough

First measure whether the workload waits on input/output or spends time computing. Choose threads or async I/O for many waiting operations and processes for CPU-heavy work, then test how cancellation, errors, and resource limits behave under load.

6. Common mistakes to avoid

  • Adding concurrency before measuring a bottleneck or without bounding the number of tasks.
  • Assuming asyncio makes CPU-heavy Python calculations run in parallel.

Visual guide

Choose concurrency by workload

  1. Step 01 Measure

    Determine whether work waits on I/O or uses CPU.

  2. Step 02 Choose

    Select threads, processes, or async APIs.

  3. Step 03 Bound

    Limit concurrency and handle cancellation.

Code examples

Worked example
PYTHON
import asyncio

async def task(name: str) -> str:
    await asyncio.sleep(0.01)
    return f'done: {name}'

async def main() -> None:
    results = await asyncio.gather(task('A'), task('B'))
    print(results)

asyncio.run(main())
Expected output
['done: A', 'done: B']
Run independent I/O tasks concurrently
PYTHON
import asyncio

async def fetch_label(label: str) -> str:
    await asyncio.sleep(0.01)
    return label

async def main() -> None:
    print(await asyncio.gather(fetch_label('A'), fetch_label('B')))

asyncio.run(main())
Expected output
['A', 'B']
Use a thread pool for blocking I/O
PYTHON
from concurrent.futures import ThreadPoolExecutor

def read_status() -> str:
    return 'ready'

with ThreadPoolExecutor(max_workers=2) as pool:
    print(pool.submit(read_status).result())
Expected output
ready

Key points

  • Choose threads, processes, or asyncio according to the workload and library support.
  • Prefer explicit, testable behavior over hidden side effects.
  • Validate assumptions at system boundaries and handle failure deliberately.

Check your understanding

Practice

Which model is commonly useful for many nonblocking I/O tasks?

Your learning progress

Finish this lesson