[{"data":1,"prerenderedAt":69},["ShallowReactive",2],{"qa-\u002Fpython\u002Fconcurrency\u002Fgil":3},{"page":4,"siblings":53,"blog":66},{"id":5,"title":6,"body":7,"description":11,"difficulty":14,"extension":15,"framework":16,"frameworkSlug":17,"meta":18,"navigation":19,"order":20,"path":21,"questions":22,"related":44,"seo":45,"seoDescription":46,"stem":47,"subtopic":48,"topic":49,"topicSlug":50,"updated":51,"__hash__":52},"qa\u002Fpython\u002Fconcurrency\u002Fgil.md","Gil",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"hard","md","Python","python",{},true,1,"\u002Fpython\u002Fconcurrency\u002Fgil",[23,27,32,36,40],{"id":24,"difficulty":14,"q":25,"a":26},"what-is-the-gil","What is the GIL?","The **Global Interpreter Lock** is a mutex in **CPython** that allows **only one\nthread to execute Python bytecode at a time**. Even on a multi-core machine, a\nmultithreaded pure-Python program runs its bytecode on **one core at a time** —\nthreads take turns holding the lock.\n\nIt exists to make CPython's memory management (especially **reference counting**)\nsimple and fast: without it, every refcount update would need its own lock. The\ninterpreter releases the GIL periodically and **around blocking I\u002FO** so other\nthreads can run.\n\n```python\nimport threading\n# both threads exist, but the GIL serializes their bytecode execution\ndef work():\n    total = 0\n    for _ in range(10_000_000):   # CPU-bound — holds the GIL\n        total += 1\n\nt1 = threading.Thread(target=work)\nt2 = threading.Thread(target=work)\nt1.start(); t2.start(); t1.join(); t2.join()   # ~no speedup vs one thread\n```\n\nWhy it matters: the GIL is a **CPython implementation detail** (not in the language\nspec, and absent in Jython\u002Fthe free-threaded 3.13+ build) that shapes when threads\ndo and don't help.\n",{"id":28,"difficulty":29,"q":30,"a":31},"threading-vs-multiprocessing","medium","What is the difference between threading and multiprocessing?","**Threads** share one process and one memory space, so they're cheap to create and\nshare data directly — but in CPython they're serialized by the **GIL**.\n**Processes** each have their own interpreter and memory, so they run on\n**separate cores in true parallel**, bypassing the GIL — at the cost of higher\nstartup overhead and needing to **serialize (pickle) data** to communicate.\n\n```python\nfrom threading import Thread\nfrom multiprocessing import Process\n\nThread(target=fn)    # shared memory, GIL-bound, light\nProcess(target=fn)   # separate memory, real parallelism, heavier\n```\n\nThreads communicate through shared objects (guarded by locks); processes\ncommunicate through `Queue`, `Pipe`, or shared-memory primitives because they don't\nshare state.\n\nRule of thumb: **threads for I\u002FO-bound** concurrency, **processes for CPU-bound**\nparallelism.\n",{"id":33,"difficulty":14,"q":34,"a":35},"cpu-vs-io-bound","Why don't threads speed up CPU-bound work but help I\u002FO-bound work?","For **CPU-bound** work, threads are constantly executing bytecode, so they're\nalways contending for the **GIL** — only one runs at a time, and you get no\nparallel speedup (often a small slowdown from lock contention and context switches).\n\nFor **I\u002FO-bound** work, a thread that's waiting on the network, disk, or a database\nis **blocked outside the interpreter** — and CPython **releases the GIL during\nblocking I\u002FO**. So other threads run while one waits, giving real concurrency.\n\n```python\nimport requests, threading\n\ndef fetch(url):\n    requests.get(url)     # blocks on the network -> GIL released here\n\n# 10 threads overlap their waiting time -> much faster than sequential\nthreads = [threading.Thread(target=fetch, args=(u,)) for u in urls]\n```\n\nRule of thumb: if your bottleneck is **waiting**, use threads (or asyncio); if it's\n**computing**, use **processes** to get past the GIL.\n",{"id":37,"difficulty":14,"q":38,"a":39},"race-conditions-locks","What is a race condition and how do locks prevent it?","A **race condition** occurs when two threads access shared mutable state and the\nresult depends on **timing**. Even `x += 1` is not atomic — it's read, add, write,\nand a thread can be switched out mid-sequence, so updates get lost.\n\nA **lock** (`threading.Lock`) creates a **critical section**: only the thread\nholding it can proceed, the rest wait, so the read-modify-write happens atomically.\nUsing `with lock:` guarantees the lock is always released, even on exceptions.\n\n```python\nimport threading\ncounter = 0\nlock = threading.Lock()\n\ndef increment():\n    global counter\n    for _ in range(100_000):\n        with lock:           # only one thread in here at a time\n            counter += 1     # now safe from lost updates\n```\n\nBeware over-locking: acquiring multiple locks in different orders can cause\n**deadlock**. Rule of thumb: guard **every** access to shared mutable state, keep\ncritical sections small, and prefer thread-safe `queue.Queue` for handoff.\n",{"id":41,"difficulty":29,"q":42,"a":43},"threadpool-vs-processpool","When do you use ThreadPoolExecutor vs ProcessPoolExecutor?","Both come from `concurrent.futures` and share the same API — `submit` \u002F\n`map` returning `Future` objects — so you can swap them with one line. The\ndifference is the **worker type**: `ThreadPoolExecutor` runs tasks in **threads**\n(shared memory, GIL-bound) and `ProcessPoolExecutor` runs them in **separate\nprocesses** (true parallelism, pickled args).\n\n```python\nfrom concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor\n\n# I\u002FO-bound: many network\u002Ffile waits -> threads\nwith ThreadPoolExecutor(max_workers=20) as ex:\n    results = list(ex.map(download, urls))\n\n# CPU-bound: heavy computation -> processes (one per core)\nwith ProcessPoolExecutor() as ex:\n    results = list(ex.map(crunch_numbers, datasets))\n```\n\nUse **ThreadPoolExecutor for I\u002FO-bound** tasks (downloads, DB calls) where waiting\ndominates, and **ProcessPoolExecutor for CPU-bound** tasks to use all cores —\nremembering its arguments and return values must be **picklable**.\n\nRule of thumb: pick the executor by the bottleneck (waiting vs computing), and let\n`concurrent.futures` handle the pool lifecycle and result collection.\n",null,{"description":11},"Python interview questions on threading and the GIL — what the GIL is, threads vs multiprocessing, CPU-bound vs I\u002FO-bound work, race conditions and locks, and ThreadPoolExecutor vs ProcessPoolExecutor.","python\u002Fconcurrency\u002Fgil","Threading & the GIL","Concurrency & Parallelism","concurrency","2026-06-18","xnP8BJ-7TKpKC-39NQwkB1eRxm7A9hp5p5zsqHw6SWE",[54,55,58,62],{"subtopic":48,"path":21,"order":20},{"subtopic":56,"path":57,"order":12},"Multiprocessing","\u002Fpython\u002Fconcurrency\u002Fmultiprocessing",{"subtopic":59,"path":60,"order":61},"asyncio & async\u002Fawait","\u002Fpython\u002Fconcurrency\u002Fasyncio",3,{"subtopic":63,"path":64,"order":65},"concurrent.futures","\u002Fpython\u002Fconcurrency\u002Fconcurrent-futures",4,{"path":67,"title":68},"\u002Fblog\u002Fpython-gil-threading-explained","Python Threading and the GIL Explained — Threads vs Multiprocessing",1781808676720]