Concurrency & GIL
The GIL, threads, processes, and async in Python.
17 questions
JuniorTheoryVery commonWhat is the GIL in CPython?
What is the GIL in CPython?
The GIL (Global Interpreter Lock) is a mutex letting only one thread execute Python bytecode at a time, protecting interpreter internals like reference counts. A thread releases it on I/O and periodically.
Common mistakes
- ✗Thinking the
GILlocks each object separately rather than the whole interpreter - ✗Believing the
GILmakes shared data thread-safe so no locks are ever needed - ✗Assuming the
GILpreventsmultiprocessingfrom running in parallel across cores
Follow-up questions
- →How does
sys.setswitchintervalchange how often theGILis released? - →Which kinds of operations let a thread drop the
GILwhile running?
MiddleTheoryVery commonHow do async/await and the event loop work?
How do async/await and the event loop work?
async def defines a coroutine; await suspends it until an awaitable completes, yielding control to a single-threaded event loop that runs other ready coroutines meanwhile. No extra threads.
Common mistakes
- ✗Thinking
awaitblocks the whole program instead of yielding to the loop - ✗Believing each coroutine runs on its own OS thread
- ✗Assuming the event loop preempts coroutines on a timer rather than at
await
Follow-up questions
- →How does
asyncio.gatherlet severalawaits overlap instead of running serially? - →What awaitable types can you legally place after the
awaitkeyword?
JuniorTheoryCommonWhat is a coroutine?
What is a coroutine?
A coroutine generalizes a subroutine: it has multiple entry points and can suspend and resume, preserving its state. In Python they use async def/await (or yield), enabling cooperative concurrency.
Common mistakes
- ✗Equating a coroutine with an OS thread the scheduler preempts
- ✗Believing coroutines run in parallel across cores instead of cooperatively
- ✗Thinking a coroutine loses its local state when it suspends at
await
Follow-up questions
- →How does a generator-based coroutine with
yielddiffer from anasync defone? - →What object does calling an
async deffunction return before youawaitit?
JuniorTheoryCommonAre Python threads real OS threads?
Are Python threads real OS threads?
Yes — the threading module creates native OS (POSIX/Windows) threads scheduled by the operating system, not interpreter-level green threads. The GIL still serializes their Python bytecode execution.
Common mistakes
- ✗Calling Python threads green threads when
threadinguses native OS threads - ✗Confusing
threading.Threadwith a separate process that has isolated memory - ✗Assuming the interpreter, not the OS scheduler, decides which thread runs
Follow-up questions
- →If they are real OS threads, why don't they parallelize CPU-bound work?
- →How does the OS scheduler interact with the
GILduring a thread switch?
JuniorTheoryCommonHow do threads and processes differ in memory?
How do threads and processes differ in memory?
Threads share one address space — the same memory, variables, and modules — so they communicate cheaply but need synchronization. Processes are independent with separate spaces and must use IPC.
Common mistakes
- ✗Swapping the roles — claiming threads are isolated and processes share memory
- ✗Thinking processes share globals the way threads in one process do
- ✗Believing threads need IPC like pipes instead of just shared memory plus locks
Follow-up questions
- →Why do shared-memory threads still need a
Lockaround mutable state? - →What does
os.forkcopy versus share when it starts a child process?
MiddleCodeCommonRun three I/O tasks concurrently with asyncio
Run three I/O tasks concurrently with asyncio
Use asyncio.gather to schedule the coroutines concurrently on one thread: await asyncio.gather(fetch(1), fetch(2), fetch(3)). await asyncio.sleep yields control so all three overlap and finish in ~1s, not 3s. A blocking time.sleep would stall the whole loop instead.
Common mistakes
- ✗Awaiting each coroutine sequentially, which serializes them to ~3s
- ✗Using
time.sleep(blocking) inside a coroutine and stalling the loop - ✗Calling
asyncio.runper coroutine instead of onegather
Follow-up questions
- →Why does a blocking call like
time.sleepdefeatasyncioconcurrency? - →How does
asyncio.gatherdiffer fromasyncio.as_completed?
MiddleCodeCommonWhy does this threaded CPU loop get no speedup?
Why does this threaded CPU loop get no speedup?
The two threads are no faster than sequential — often slower, from GIL contention — because count is pure-Python CPU work and the GIL lets only one thread run bytecode at a time. To parallelize, use multiprocessing.Process or a ProcessPoolExecutor.
Common mistakes
- ✗Believing threads give CPU parallelism in CPython
- ✗Thinking the GIL only affects I/O, not CPU loops
- ✗Expecting a longer loop to overcome the GIL
Follow-up questions
- →Why does the GIL not hurt I/O-bound threaded code the same way?
- →How does
multiprocessingachieve real CPU parallelism that threads cannot?
MiddleTheoryCommonWhich tasks benefit from threads — I/O-bound or CPU-bound?
Which tasks benefit from threads — I/O-bound or CPU-bound?
I/O-bound tasks benefit: a thread releases the GIL while waiting on network or disk, letting others run, so concurrency hides latency. CPU-bound tasks don't — the GIL serializes them.
Common mistakes
- ✗Expecting threads to parallelize CPU-bound loops across cores
- ✗Concluding threads are useless everywhere because of the
GIL - ✗Thinking a thread keeps the
GILwhile blocked on I/O
Follow-up questions
- →How would
multiprocessingchange the answer for CPU-bound work? - →Why can
asyncioreplace threads for many concurrent I/O waits?
MiddleTheoryCommonWhy use multiprocessing for CPU-bound work?
Why use multiprocessing for CPU-bound work?
Each process has its own interpreter and its own GIL, so processes run Python bytecode in true parallel across cores, sidestepping the single-GIL serialization that throttles threads on CPU-bound work.
Common mistakes
- ✗Believing worker processes share one global
GILlike threads do - ✗Treating
multiprocessingas justthreadingwith nicer syntax - ✗Thinking it helps only I/O-bound and not CPU-bound work
Follow-up questions
- →Why must arguments and return values be picklable across the process boundary?
- →When does process startup and IPC overhead outweigh the parallelism gain?
MiddleDebuggingCommonWhy is this threaded counter wrong, and how to fix it?
Why is this threaded counter wrong, and how to fix it?
Race condition: counter += 1 is a read-modify-write — not atomic at the bytecode level — so threads interleave and lose updates (the GIL guarantees per-bytecode atomicity, not multi-step operations). Fix: guard the increment with a threading.Lock (with lock: counter += 1).
Common mistakes
- ✗Believing the GIL makes
+=atomic across threads - ✗Blaming print timing rather than lost updates
- ✗Thinking
globalalone makes the increment thread-safe
Follow-up questions
- →Why does the GIL guarantee bytecode atomicity but not statement atomicity?
- →When would
itertools.countor aqueue.Queuebe a cleaner fix than a lock?
MiddleDebuggingOccasionalWhy can these two locks deadlock?
Why can these two locks deadlock?
Lock-ordering inversion: t1 takes a then b, t2 takes b then a. If each acquires its first lock at once, neither can get the second → deadlock. Fix: acquire locks in a single consistent global order everywhere, or use a timeout (lock.acquire(timeout=...)).
Common mistakes
- ✗Believing short critical sections cannot deadlock
- ✗Confusing this with a non-reentrant single-lock re-acquire
- ✗Blaming the GIL rather than the inverted lock order
Follow-up questions
- →Why does a single consistent global lock ordering prevent deadlock?
- →How does acquiring with a timeout let a thread recover from a potential deadlock?
MiddleTheoryOccasionalWhat are greenlets / green threads?
What are greenlets / green threads?
Lightweight user-space threads scheduled by the runtime or library (e.g. greenlet, gevent), invisible to the OS — to it the process looks single-threaded. The library switches them cooperatively.
Common mistakes
- ✗Calling greenlets OS threads the kernel schedules
- ✗Believing greenlets bypass the
GILfor parallel CPU-bound work - ✗Thinking the OS scheduler preempts greenlets instead of cooperative switching
Follow-up questions
- →How does
geventmonkey-patch blocking calls to yield at I/O points? - →What happens to other greenlets if one runs a tight CPU loop without yielding?
MiddleCodeOccasionalParallelize CPU-bound work with a process pool
Parallelize CPU-bound work with a process pool
Use ProcessPoolExecutor to sidestep the GIL: with ProcessPoolExecutor() as pool: results = list(pool.map(square, range(10))). Each process has its own interpreter and GIL, so the work runs truly in parallel. Functions and arguments must be picklable to cross the boundary.
Common mistakes
- ✗Using threads for CPU-bound work (the GIL blocks parallelism)
- ✗Passing a non-picklable function or argument across processes
- ✗Submitting the whole iterable as one argument instead of mapping per item
Follow-up questions
- →Why must the function and arguments be picklable for
ProcessPoolExecutor? - →When is
ThreadPoolExecutorthe right choice instead ofProcessPoolExecutor?
SeniorTheoryOccasionalWhat happens if you run blocking code inside a coroutine?
What happens if you run blocking code inside a coroutine?
It blocks the entire event loop: one thread runs all coroutines cooperatively and only await yields control, so a synchronous blocking call stalls every other coroutine until it returns.
Common mistakes
- ✗Believing the loop preempts a blocking coroutine on a timeout
- ✗Thinking only the blocking coroutine pauses while siblings keep running
- ✗Assuming sync calls are auto-offloaded to a thread inside
async def
Follow-up questions
- →How does
loop.run_in_executorkeep a blocking call off the event loop? - →Why is an
async-native client preferable to wrapping a sync one in a thread?
SeniorDesignOccasionalYou have a CSV of the top 100,000 sites (rank, url). Design an asynchronous Python program that fetches each site and tallies which web server it runs (nginx, apache, IIS, unknown). Explain the concurrency primitives you would use, how you bound the number of simultaneous requests, how you handle timeouts and failures, and which resource (memory, CPU, or sockets) you expect to run out of first and why.
You have a CSV of the top 100,000 sites (rank, url). Design an asynchronous Python program that fetches each site and tallies which web server it runs (nginx, apache, IIS, unknown). Explain the concurrency primitives you would use, how you bound the number of simultaneous requests, how you handle timeouts and failures, and which resource (memory, CPU, or sockets) you expect to run out of first and why.
Use asyncio with an async HTTP client (e.g. aiohttp), reading the Server response header per site. Bound concurrency with an asyncio.Semaphore — without it you exhaust sockets/file descriptors. Set a per-request timeout and wrap each fetch in try/except so one failure does not sink the batch (count it as unknown). Stream the CSV rather than loading it all. Sockets/FDs run out first: the work is I/O-bound, so CPU and memory stay low while open connections pile up.
Common mistakes
- ✗Firing all requests at once with no semaphore, exhausting sockets/FDs
- ✗Reaching for processes or threads on an I/O-bound workload
- ✗Letting one failed request abort the whole batch instead of counting it
Follow-up questions
- →Why does an
asyncio.Semaphoreprevent socket/file-descriptor exhaustion? - →How would you add bounded retries for transient network failures?
SeniorTheoryOccasionalCan adding threads make a CPU-bound CPython loop run slower than a single thread, and why?
Can adding threads make a CPU-bound CPython loop run slower than a single thread, and why?
Yes. The GIL already serializes the bytecode, so there is no speedup; on top of that the runtime keeps forcing GIL acquire/release handoffs between contending threads, and that pure switching overhead can drag total throughput below the single-threaded baseline.
Common mistakes
- ✗Assuming extra threads can at worst tie single-threaded, never lose
- ✗Blaming the slowdown on cache contention rather than
GILhandoffs - ✗Expecting
sys.setswitchintervaltuning to restore CPU-bound speedup
Follow-up questions
- →How does a C extension releasing the
GILenable real parallel computation? - →Why can
multiprocessingscale CPU-bound work where threads cannot?
MiddleDesignRareYou must poll 1,000,000 URLs, then for each result call three independent network services and combine them with an already-written business_logic(s1, s2, s3), finally saving every result. The function makes only network calls (no CPU work). Which concurrency model — single-threaded, threads, processes, or asyncio — fits best, and why? Explain how the choice scales to a million high-fan-out I/O requests, why the others fall short, and how you would bound the number of simultaneous connections.
You must poll 1,000,000 URLs, then for each result call three independent network services and combine them with an already-written business_logic(s1, s2, s3), finally saving every result. The function makes only network calls (no CPU work). Which concurrency model — single-threaded, threads, processes, or asyncio — fits best, and why? Explain how the choice scales to a million high-fan-out I/O requests, why the others fall short, and how you would bound the number of simultaneous connections.
Use asyncio: a single-threaded event loop handles vast numbers of concurrent I/O waits cheaply, which is exactly this workload. Threads add OS-thread overhead and context-switch cost that does not scale to a million; processes are for CPU-bound work and waste memory here; single-threaded is far too slow. Bound concurrency with an asyncio.Semaphore so you don't exhaust sockets or file descriptors, and gather results per URL.
Common mistakes
- ✗Reaching for
multiprocessingon an I/O-bound, not CPU-bound, workload - ✗Believing a million OS threads scale without overhead
- ✗Omitting a semaphore, so connections exhaust sockets or file descriptors
Follow-up questions
- →Why does the event loop scale to a million I/O waits where threads do not?
- →If the three services fail with 1% probability and calls are idempotent, how would you add bounded retries?