parallel computing
concurrency
task execution
multiprocessing
performance optimization

Executing tasks in parallel

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Executing tasks in parallel means allowing multiple units of work to make progress at the same time. It can reduce total runtime significantly, but only when the work is independent enough and the coordination overhead is justified. The practical question is not whether parallelism is possible, but whether it actually improves the workload you have.

Concurrency Versus Parallelism

These terms are related but not identical.

  • concurrency means tasks overlap in progress
  • parallelism means tasks literally run at the same time on separate workers or cores

That distinction matters because I/O-heavy work and CPU-heavy work benefit from different tools. Waiting on network responses is very different from numerically heavy computation.

Parallel I/O Work With Threads

When tasks spend most of their time waiting on I/O, threads are often enough. The waits can overlap even if little CPU work is happening.

python
1from concurrent.futures import ThreadPoolExecutor
2import time
3
4
5def fetch(name: str) -> str:
6    time.sleep(1)
7    return f"done:{name}"
8
9
10with ThreadPoolExecutor(max_workers=3) as pool:
11    results = list(pool.map(fetch, ["a", "b", "c"]))
12
13print(results)

This finishes much faster than three sequential one-second waits.

CPU-Bound Work Needs a Different Strategy

For CPU-heavy work in Python, processes are often better than threads because of the GIL.

python
1from concurrent.futures import ProcessPoolExecutor
2
3
4def square_sum(n: int) -> int:
5    total = 0
6    for i in range(n):
7        total += i * i
8    return total
9
10
11with ProcessPoolExecutor(max_workers=4) as pool:
12    results = list(pool.map(square_sum, [200_000, 200_000, 200_000, 200_000]))
13
14print(results)

Processes can use multiple CPU cores instead of contending inside one interpreter thread.

Ordered Results Versus Completion Order

Some parallel APIs preserve submission order, while others give you tasks as soon as they finish. Choose intentionally.

Ordered with map:

python
1from concurrent.futures import ThreadPoolExecutor
2import time
3
4
5def work(x):
6    time.sleep(1 - x * 0.2)
7    return x
8
9
10with ThreadPoolExecutor(max_workers=3) as pool:
11    print(list(pool.map(work, [1, 2, 3])))

Completion order with as_completed:

python
1from concurrent.futures import ThreadPoolExecutor, as_completed
2
3
4with ThreadPoolExecutor(max_workers=3) as pool:
5    futures = [pool.submit(work, x) for x in [1, 2, 3]]
6    for future in as_completed(futures):
7        print(future.result())

If you need results in original order, use APIs that preserve it.

Handle Errors Explicitly

Parallel failures are easy to miss if you do not inspect worker results.

python
1from concurrent.futures import ThreadPoolExecutor, as_completed
2
3
4def maybe_fail(x):
5    if x == 2:
6        raise ValueError("bad task")
7    return x * 10
8
9
10with ThreadPoolExecutor(max_workers=3) as pool:
11    futures = [pool.submit(maybe_fail, x) for x in [1, 2, 3]]
12    for future in as_completed(futures):
13        try:
14            print("result:", future.result())
15        except Exception as exc:
16            print("task failed:", exc)

Without explicit exception handling, one failed worker can quietly poison the whole run.

When Parallel Execution Is Not Worth It

Parallelism adds overhead:

  • worker startup cost
  • synchronization complexity
  • memory duplication
  • harder debugging and testing

If each task is tiny, the overhead may exceed the benefit. Measure before you parallelize.

Design Guidelines That Hold Up

A practical rule set:

  1. use threads or async tools for I/O-bound workloads
  2. use processes for heavy CPU-bound work in Python
  3. avoid shared mutable state across workers
  4. benchmark with realistic data sizes

These rules are more useful than generic advice to make everything parallel.

Common Pitfalls

One common mistake is using Python threads for CPU-heavy work and expecting linear speedup. Another is assuming results arrive in input order when using completion-based APIs. Developers also skip exception handling and lose visibility into failed tasks. Shared mutable state can introduce race conditions that are far worse than sequential slowness. Finally, many microbenchmarks exaggerate speedup because they do not reflect real workloads.

Summary

  • Parallel execution helps when tasks are independent and large enough to justify overhead.
  • Use threads for I/O-bound work and processes for CPU-bound Python work.
  • Choose ordered or completion-based result handling deliberately.
  • Always collect exceptions from worker tasks.
  • Avoid shared mutable state unless synchronization is designed carefully.
  • Measure real speedup before committing to a parallel architecture.

Course illustration
Course illustration

All Rights Reserved.