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
-
Step 01
Measure
Determine whether work waits on I/O or uses CPU.
-
Step 02
Choose
Select threads, processes, or async APIs.
-
Step 03
Bound
Limit concurrency and handle cancellation.
Code examples
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())
['done: A', 'done: B']
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())
['A', 'B']
from concurrent.futures import ThreadPoolExecutor
def read_status() -> str:
return 'ready'
with ThreadPoolExecutor(max_workers=2) as pool:
print(pool.submit(read_status).result())
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
Your learning progress