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.
Python code
35 linesimport 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
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
- Use asyncio.gather with async functions for single-threaded async concurrency
- 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
More from Concurrency & performance
- Benchmark list.append vs deque.append in Python medium
- Build a Python Performance Profiler That Generates Readable Reports medium
- Graceful Shutdown Executor Context Manager in Python medium
- How to Build a Producer-Consumer Pattern with asyncio.Queue in Python medium
- How to Cancel an asyncio Task with Graceful Cleanup in Python medium
- How to Convert Data in Parallel with ThreadPoolExecutor in Python easy
Keep learning
Related tutorials and quizzes for this topic.