How to Use ThreadPoolExecutor for Concurrent Tasks in Python

Compare sequential execution with ThreadPoolExecutor for I/O-bound tasks, measuring speedup and timing with perf_counter.

Medium Python 3.9+ Aug 9, 2026 Concurrency & performance 14 views 0 copies

Python code

35 lines
Python 3.9+
import time
import threading
from concurrent.futures import ThreadPoolExecutor


def fetch_data(index):
    """Simulate a synchronous data fetch."""
    time.sleep(0.1)
    return f"data-{index}"


def run_sequential(total=10):
    """Run tasks one after another."""
    start = time.perf_counter()
    results = [fetch_data(i) for i in range(total)]
    elapsed = time.perf_counter() - start
    return results, elapsed


def run_threaded(total=10, workers=4):
    """Run tasks concurrently using a thread pool."""
    start = time.perf_counter()
    with ThreadPoolExecutor(max_workers=workers) as executor:
        results = list(executor.map(fetch_data, range(total)))
    elapsed = time.perf_counter() - start
    return results, elapsed


if __name__ == "__main__":
    seq_results, seq_time = run_sequential()
    thr_results, thr_time = run_threaded()

    print(f"Sequential: {seq_time:.3f}s -> {seq_results[:3]}...")
    print(f"Threaded  : {thr_time:.3f}s -> {thr_results[:3]}...")
    print(f"Speedup   : {seq_time / thr_time:.2f}x")

Output

stdout
Sequential: 1.001s -> ['data-0', 'data-1', 'data-2']...
Threaded  : 0.251s -> ['data-0', 'data-1', 'data-2']...
Speedup   : 3.99x

How it works

time.sleep(0.1) simulates an I/O-bound wait like a network call, which releases the GIL and lets threads overlap. ThreadPoolExecutor.map submits all calls, runs them concurrently across a fixed worker pool, and returns results in input order. time.perf_counter gives high-resolution wall-clock timing ideal for comparing execution strategies. The speedup approaches the worker count (4) because the simulated delay dominates runtime and threads switch efficiently.

Common mistakes

  • Using threads for CPU-bound work where the GIL prevents parallel speedup
  • Calling executor.submit without collecting futures, causing silent early exits
  • Forgetting to use a context manager, leaking threads on exceptions
  • Measuring with time.time() which has lower resolution on some platforms

Variations

  1. Use asyncio.gather with async functions for single-threaded async concurrency
  2. Use ProcessPoolExecutor for CPU-bound parallelism across multiple cores

Real-world use cases

  • Fetching multiple API endpoints in a data pipeline to reduce total latency by running requests in parallel.
  • Downloading a batch of files or images in a script where network I/O dominates and threads provide speedup.
  • Running independent database queries concurrently in a report generator to cut wall-clock time.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Concurrency & performance

Related tutorials and quizzes for this topic.