ThreadPoolExecutor and as_completed
advanced20 min readLesson 122 of 169
Run bounded concurrent work with one modern API โ and know both iteration contracts.
Executors: concurrent.futures as the default interface
concurrent.futures wraps both threading and multiprocessing behind one API โ
and it is the right default for most programs:
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch(url):
... # I/O-bound work
with ThreadPoolExecutor(max_workers=8) as pool:
futures = {pool.submit(fetch, u): u for u in urls}
for fut in as_completed(futures):
url = futures[fut]
try:
result = fut.result()
except Exception as e:
print(f"{url} failed: {e}")
Two iteration styles with different guarantees:
pool.map(fn, iterable)โ results in INPUT order, exceptions raised when you consume the result, simplest for pure transforms.as_completed(futures)โ results in COMPLETION order (fast items first), the choice when per-item latency matters and you want progress as items finish.
Professional details that bite in production:
fut.result(timeout=...)bounds your wait; a hung task still holds the worker thread forever โ cancellation of running threads is cooperative, not forced.Executor.shutdown(cancel_futures=True)(3.9+) drops queued-but-not-started work on teardown.- Always use
with(or explicit shutdown): leaked executors keep interpreter exit waiting. ProcessPoolExecutorhas the same API but pickles arguments and results โ jobs must be picklable top-level functions; great for CPU-bound work (each process has its own GIL).