Run several concurrently, collect all results in order
TaskGroup
async with asyncio.TaskGroup() as tg: tg.create_task(...)
Structured concurrency; auto-cancels siblings on first exception
asyncio.sleep
await asyncio.sleep(n)
Non-blocking wait, yields to loop
Lock/Event/Semaphore
await lock.acquire() etc.
Same concepts as threading's, but await-based
to_thread
await asyncio.to_thread(fn, *args)
Run blocking sync fn on a worker thread
run_in_executor
await loop.run_in_executor(executor, fn, *args)
Same, lets you pass your own Executor (thread/process)
concurrent.futures
Construct
Signature
What it does
ThreadPoolExecutor
ThreadPoolExecutor(max_workers=n)
Pool of reused threads
ProcessPoolExecutor
ProcessPoolExecutor(max_workers=n)
Pool of reused processes
.submit
.submit(fn, *args) → Future
1 call, 1 Future, heterogeneous tasks
.map
.map(fn, *iterables)
Same fn across N inputs (zipped), ordered, lazy iterator
Future.result
.result(timeout=None)
Block for value; re-raises worker's exception
Future.exception
.exception(timeout=None)
Block for exception object; no raise
Future.done/running/cancelled
.done() etc.
State queries
Future.cancel
.cancel() → bool
Works only if still PENDING
Future.add_done_callback
.add_done_callback(fn)
Fire fn(future) on completion
as_completed
as_completed(futures, timeout=None)
Yield futures one at a time, completion order
wait
wait(futures, timeout=None, return_when=...)
One blocking call → (done, not_done); never raises on timeout
initializer/initargs
kwarg on ProcessPoolExecutor
Same pattern as Pool's
chunksize
kwarg on ProcessPoolExecutor.map
Batch items per IPC round-trip
2. Heavily commented snippets
threading
importthreadingcounter=0lock=threading.Lock()defincrement():globalcounterfor_inrange(100_000):withlock:# acquire before touching shared state, auto-release after block
counter+=1# NOT atomic without the lock — GIL switches mid-operation
threads=[threading.Thread(target=increment)for_inrange(4)]# 4 OS threads, same process
fortinthreads:t.start()# start() schedules it; does NOT block
fortinthreads:t.join()# block main thread until this one finishes; no return value available
print(counter)# 400000 — correct only because of the lock
multiprocessing
frommultiprocessingimportPool,Value,Lockdefcpu_heavy(n):returnsum(i*iforiinrange(n))# pure Python CPU work — needs real processes to parallelize
if__name__=="__main__":# REQUIRED guard — spawn re-imports this module
withPool(processes=4)aspool:# 4 worker OS processes, separate interpreters/GILs
results=pool.map(cpu_heavy,[10**6]*4)# 1 iterable, 1 arg per call, blocks, returns list
# pool.starmap(fn, [(a1,b1),(a2,b2)]) -> unpacks each tuple as multiple positional args
print(results)shared_val=Value('i',0)# 'i' = C int, lives in shared memory across processes
shared_lock=Lock()# multiprocessing's OWN Lock — NOT threading.Lock, not interchangeable
# pass both via initializer/initargs to workers that need to mutate shared_val safely
asyncio
importasyncioasyncdeffetch(n):print(f"start {n}")awaitasyncio.sleep(1)# NON-blocking wait — yields control back to loop, does NOT occupy the thread
print(f"done {n}")returnn*2asyncdefmain():# create_task schedules immediately, runs concurrently — NOT the same as multiprocessing's Process/Pool
task=asyncio.create_task(fetch(99))results=awaitasyncio.gather(*[fetch(i)foriinrange(3)])# run many concurrently, ordered results
print(results)awaittask# must await it eventually or it may be GC'd before completing
# blocking sync call inside async code — WOULD freeze the whole loop if awaited directly:
# time.sleep(2) # <-- never do this inside a coroutine
awaitasyncio.to_thread(lambda:None)# correct way: offload blocking work to a thread, don't block loop
asyncio.run(main())# starts the loop, runs main() to completion, only entry point needed
concurrent.futures
fromconcurrent.futuresimportThreadPoolExecutor,as_completed,wait,FIRST_EXCEPTIONdeffetch(url):returnf"fetched {url}"# any regular blocking callable — no async needed
withThreadPoolExecutor(max_workers=4)asex:# __exit__ calls shutdown(wait=True) — blocks til ALL done
future=ex.submit(fetch,"a.com")# 1 call -> 1 Future, heterogeneous tasks OK
print(future.result())# blocks for THIS future's value; re-raises its exception
futures={ex.submit(fetch,u):uforuin["a.com","b.com"]}# dict: Future -> original input
forfinas_completed(futures):# yields in COMPLETION order, not submission order
print(futures[f],"->",f.result())done,not_done=wait(futures,timeout=5,return_when=FIRST_EXCEPTION)# wait() NEVER raises on timeout — just returns whatever split exists; not_done may be non-empty
# FIRST_EXCEPTION stops WAITING early, does NOT cancel/stop the still-running not_done futures
Top comments (0)
Subscribe
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
Top comments (0)