[{"data":1,"prerenderedAt":803},["ShallowReactive",2],{"blog-\u002Fblog\u002Fpython-gil-threading-explained":3},{"id":4,"title":5,"body":6,"description":789,"difficulty":790,"extension":791,"framework":792,"frameworkSlug":47,"meta":793,"navigation":70,"order":56,"path":794,"qaPath":795,"seo":796,"stem":797,"subtopic":798,"topic":799,"topicSlug":800,"updated":801,"__hash__":802},"blog\u002Fblog\u002Fpython-gil-threading-explained.md","Python Threading and the GIL Explained — Threads vs Multiprocessing",{"type":7,"value":8,"toc":778},"minimark",[9,14,23,27,42,185,192,196,203,218,225,234,249,363,370,374,389,482,493,497,510,621,624,628,634,735,742,746,774],[10,11,13],"h2",{"id":12},"the-gil-explained","The GIL, explained",[15,16,17,18,22],"p",{},"No Python concurrency conversation gets far before the ",[19,20,21],"strong",{},"Global Interpreter Lock"," comes\nup, and few topics are as widely misunderstood. The GIL explains why adding threads to a\nnumber-crunching program makes it no faster (sometimes slower), yet threads remain the\nright tool for network and disk work. This guide makes the model concrete and shows when\nto choose threads, processes, or async.",[10,24,26],{"id":25},"what-the-gil-is","What the GIL is",[15,28,29,30,33,34,37,38,41],{},"The ",[19,31,32],{},"GIL"," is a single lock in CPython that allows ",[19,35,36],{},"only one thread to execute Python\nbytecode at a time",". Even on an 8-core machine, your Python threads take turns holding the\nGIL — so pure-Python code runs effectively ",[19,39,40],{},"one thread at a time",".",[43,44,49],"pre",{"className":45,"code":46,"language":47,"meta":48,"style":48},"language-python shiki shiki-themes github-light github-dark","import threading\n\ndef crunch():\n    total = 0\n    for _ in range(10_000_000):\n        total += 1          # pure-Python bytecode — needs the GIL\n\n# Two threads do NOT run this in parallel; they alternate holding the GIL.\nt1 = threading.Thread(target=crunch)\nt2 = threading.Thread(target=crunch)\n","python","",[50,51,52,65,72,85,98,122,138,143,149,169],"code",{"__ignoreMap":48},[53,54,57,61],"span",{"class":55,"line":56},"line",1,[53,58,60],{"class":59},"szBVR","import",[53,62,64],{"class":63},"sVt8B"," threading\n",[53,66,68],{"class":55,"line":67},2,[53,69,71],{"emptyLinePlaceholder":70},true,"\n",[53,73,75,78,82],{"class":55,"line":74},3,[53,76,77],{"class":59},"def",[53,79,81],{"class":80},"sScJk"," crunch",[53,83,84],{"class":63},"():\n",[53,86,88,91,94],{"class":55,"line":87},4,[53,89,90],{"class":63},"    total ",[53,92,93],{"class":59},"=",[53,95,97],{"class":96},"sj4cs"," 0\n",[53,99,101,104,107,110,113,116,119],{"class":55,"line":100},5,[53,102,103],{"class":59},"    for",[53,105,106],{"class":63}," _ ",[53,108,109],{"class":59},"in",[53,111,112],{"class":96}," range",[53,114,115],{"class":63},"(",[53,117,118],{"class":96},"10_000_000",[53,120,121],{"class":63},"):\n",[53,123,125,128,131,134],{"class":55,"line":124},6,[53,126,127],{"class":63},"        total ",[53,129,130],{"class":59},"+=",[53,132,133],{"class":96}," 1",[53,135,137],{"class":136},"sJ8bj","          # pure-Python bytecode — needs the GIL\n",[53,139,141],{"class":55,"line":140},7,[53,142,71],{"emptyLinePlaceholder":70},[53,144,146],{"class":55,"line":145},8,[53,147,148],{"class":136},"# Two threads do NOT run this in parallel; they alternate holding the GIL.\n",[53,150,152,155,157,160,164,166],{"class":55,"line":151},9,[53,153,154],{"class":63},"t1 ",[53,156,93],{"class":59},[53,158,159],{"class":63}," threading.Thread(",[53,161,163],{"class":162},"s4XuR","target",[53,165,93],{"class":59},[53,167,168],{"class":63},"crunch)\n",[53,170,172,175,177,179,181,183],{"class":55,"line":171},10,[53,173,174],{"class":63},"t2 ",[53,176,93],{"class":59},[53,178,159],{"class":63},[53,180,163],{"class":162},[53,182,93],{"class":59},[53,184,168],{"class":63},[15,186,187,188,191],{},"It exists because CPython's memory management (reference counting) isn't thread-safe. A\nsingle global lock is the simplest way to keep refcounts correct, and it makes\nsingle-threaded code fast and C extensions easy to write. It's a ",[19,189,190],{},"CPython implementation\ndetail",", not part of the language — Jython and (early) IronPython have no GIL.",[10,193,195],{"id":194},"why-threads-dont-speed-up-cpu-bound-work","Why threads don't speed up CPU-bound work",[15,197,198,199,202],{},"Because only one thread runs Python bytecode at once, splitting a ",[19,200,201],{},"CPU-bound"," task across\nthreads gives no speedup — the threads serialise on the GIL, and you even pay extra for\nlock contention and context switching.",[43,204,206],{"className":45,"code":205,"language":47,"meta":48,"style":48},"# A CPU-bound job across 4 threads runs about the same as 1 thread\n# (often slightly slower) because the GIL serialises them.\n",[50,207,208,213],{"__ignoreMap":48},[53,209,210],{"class":55,"line":56},[53,211,212],{"class":136},"# A CPU-bound job across 4 threads runs about the same as 1 thread\n",[53,214,215],{"class":55,"line":67},[53,216,217],{"class":136},"# (often slightly slower) because the GIL serialises them.\n",[15,219,220,221,224],{},"For CPU-bound work you need true parallelism, which means ",[19,222,223],{},"separate processes"," — each\nprocess has its own interpreter and its own GIL.",[10,226,228,229,233],{"id":227},"where-threads-do-help-io-bound-work","Where threads ",[230,231,232],"em",{},"do"," help: I\u002FO-bound work",[15,235,236,237,240,241,244,245,248],{},"The crucial detail: the GIL is ",[19,238,239],{},"released during blocking I\u002FO"," — network requests, disk\nreads, ",[50,242,243],{},"time.sleep",", database calls. While one thread waits on a socket, another can run.\nSo for ",[19,246,247],{},"I\u002FO-bound"," programs, threads deliver real concurrency.",[43,250,252],{"className":45,"code":251,"language":47,"meta":48,"style":48},"import threading, urllib.request\n\ndef fetch(url):\n    with urllib.request.urlopen(url) as r:   # GIL released while waiting on the network\n        return r.read()\n\nthreads = [threading.Thread(target=fetch, args=(u,)) for u in urls]\nfor t in threads: t.start()\nfor t in threads: t.join()\n",[50,253,254,261,265,275,292,300,304,340,352],{"__ignoreMap":48},[53,255,256,258],{"class":55,"line":56},[53,257,60],{"class":59},[53,259,260],{"class":63}," threading, urllib.request\n",[53,262,263],{"class":55,"line":67},[53,264,71],{"emptyLinePlaceholder":70},[53,266,267,269,272],{"class":55,"line":74},[53,268,77],{"class":59},[53,270,271],{"class":80}," fetch",[53,273,274],{"class":63},"(url):\n",[53,276,277,280,283,286,289],{"class":55,"line":87},[53,278,279],{"class":59},"    with",[53,281,282],{"class":63}," urllib.request.urlopen(url) ",[53,284,285],{"class":59},"as",[53,287,288],{"class":63}," r:   ",[53,290,291],{"class":136},"# GIL released while waiting on the network\n",[53,293,294,297],{"class":55,"line":100},[53,295,296],{"class":59},"        return",[53,298,299],{"class":63}," r.read()\n",[53,301,302],{"class":55,"line":124},[53,303,71],{"emptyLinePlaceholder":70},[53,305,306,309,311,314,316,318,321,324,326,329,332,335,337],{"class":55,"line":140},[53,307,308],{"class":63},"threads ",[53,310,93],{"class":59},[53,312,313],{"class":63}," [threading.Thread(",[53,315,163],{"class":162},[53,317,93],{"class":59},[53,319,320],{"class":63},"fetch, ",[53,322,323],{"class":162},"args",[53,325,93],{"class":59},[53,327,328],{"class":63},"(u,)) ",[53,330,331],{"class":59},"for",[53,333,334],{"class":63}," u ",[53,336,109],{"class":59},[53,338,339],{"class":63}," urls]\n",[53,341,342,344,347,349],{"class":55,"line":145},[53,343,331],{"class":59},[53,345,346],{"class":63}," t ",[53,348,109],{"class":59},[53,350,351],{"class":63}," threads: t.start()\n",[53,353,354,356,358,360],{"class":55,"line":151},[53,355,331],{"class":59},[53,357,346],{"class":63},[53,359,109],{"class":59},[53,361,362],{"class":63}," threads: t.join()\n",[15,364,365,366,369],{},"Rule of thumb: ",[19,367,368],{},"threads for I\u002FO-bound, processes for CPU-bound."," The GIL only bites when\nthreads are fighting over Python bytecode, not when they're parked waiting on the outside\nworld.",[10,371,373],{"id":372},"race-conditions-and-locks","Race conditions and locks",[15,375,376,377,380,381,384,385,388],{},"The GIL does ",[19,378,379],{},"not"," make your code automatically thread-safe. A statement like\n",[50,382,383],{},"counter += 1"," is several bytecode steps (read, add, store), and a thread switch in the\nmiddle leads to ",[19,386,387],{},"lost updates"," — a classic race condition.",[43,390,392],{"className":45,"code":391,"language":47,"meta":48,"style":48},"import threading\n\ncounter = 0\nlock = threading.Lock()\n\ndef increment():\n    global counter\n    for _ in range(100_000):\n        with lock:           # serialise the read-modify-write\n            counter += 1\n",[50,393,394,400,404,413,423,427,436,444,461,472],{"__ignoreMap":48},[53,395,396,398],{"class":55,"line":56},[53,397,60],{"class":59},[53,399,64],{"class":63},[53,401,402],{"class":55,"line":67},[53,403,71],{"emptyLinePlaceholder":70},[53,405,406,409,411],{"class":55,"line":74},[53,407,408],{"class":63},"counter ",[53,410,93],{"class":59},[53,412,97],{"class":96},[53,414,415,418,420],{"class":55,"line":87},[53,416,417],{"class":63},"lock ",[53,419,93],{"class":59},[53,421,422],{"class":63}," threading.Lock()\n",[53,424,425],{"class":55,"line":100},[53,426,71],{"emptyLinePlaceholder":70},[53,428,429,431,434],{"class":55,"line":124},[53,430,77],{"class":59},[53,432,433],{"class":80}," increment",[53,435,84],{"class":63},[53,437,438,441],{"class":55,"line":140},[53,439,440],{"class":59},"    global",[53,442,443],{"class":63}," counter\n",[53,445,446,448,450,452,454,456,459],{"class":55,"line":145},[53,447,103],{"class":59},[53,449,106],{"class":63},[53,451,109],{"class":59},[53,453,112],{"class":96},[53,455,115],{"class":63},[53,457,458],{"class":96},"100_000",[53,460,121],{"class":63},[53,462,463,466,469],{"class":55,"line":151},[53,464,465],{"class":59},"        with",[53,467,468],{"class":63}," lock:           ",[53,470,471],{"class":136},"# serialise the read-modify-write\n",[53,473,474,477,479],{"class":55,"line":171},[53,475,476],{"class":63},"            counter ",[53,478,130],{"class":59},[53,480,481],{"class":96}," 1\n",[15,483,484,485,488,489,492],{},"Protect shared mutable state with a ",[50,486,487],{},"Lock"," (or use thread-safe primitives like\n",[50,490,491],{},"queue.Queue","). Without the lock, two threads can read the same value, both add one, and\none increment vanishes.",[10,494,496],{"id":495},"multiprocessing-real-parallelism","Multiprocessing: real parallelism",[15,498,499,502,503,505,506,509],{},[50,500,501],{},"multiprocessing"," sidesteps the GIL by running ",[19,504,223],{},", each with its own\ninterpreter and memory. That gives genuine multi-core parallelism for CPU-bound work — at\nthe cost of process startup and ",[19,507,508],{},"pickling"," data to pass between processes.",[43,511,513],{"className":45,"code":512,"language":47,"meta":48,"style":48},"from multiprocessing import Pool\n\ndef square(n):\n    return n * n\n\nif __name__ == \"__main__\":           # required guard on Windows\u002Fspawn\n    with Pool(4) as pool:\n        print(pool.map(square, range(10)))   # runs across 4 real cores\n",[50,514,515,528,532,542,556,560,581,599],{"__ignoreMap":48},[53,516,517,520,523,525],{"class":55,"line":56},[53,518,519],{"class":59},"from",[53,521,522],{"class":63}," multiprocessing ",[53,524,60],{"class":59},[53,526,527],{"class":63}," Pool\n",[53,529,530],{"class":55,"line":67},[53,531,71],{"emptyLinePlaceholder":70},[53,533,534,536,539],{"class":55,"line":74},[53,535,77],{"class":59},[53,537,538],{"class":80}," square",[53,540,541],{"class":63},"(n):\n",[53,543,544,547,550,553],{"class":55,"line":87},[53,545,546],{"class":59},"    return",[53,548,549],{"class":63}," n ",[53,551,552],{"class":59},"*",[53,554,555],{"class":63}," n\n",[53,557,558],{"class":55,"line":100},[53,559,71],{"emptyLinePlaceholder":70},[53,561,562,565,568,571,575,578],{"class":55,"line":124},[53,563,564],{"class":59},"if",[53,566,567],{"class":96}," __name__",[53,569,570],{"class":59}," ==",[53,572,574],{"class":573},"sZZnC"," \"__main__\"",[53,576,577],{"class":63},":           ",[53,579,580],{"class":136},"# required guard on Windows\u002Fspawn\n",[53,582,583,585,588,591,594,596],{"class":55,"line":140},[53,584,279],{"class":59},[53,586,587],{"class":63}," Pool(",[53,589,590],{"class":96},"4",[53,592,593],{"class":63},") ",[53,595,285],{"class":59},[53,597,598],{"class":63}," pool:\n",[53,600,601,604,607,610,612,615,618],{"class":55,"line":145},[53,602,603],{"class":96},"        print",[53,605,606],{"class":63},"(pool.map(square, ",[53,608,609],{"class":96},"range",[53,611,115],{"class":63},[53,613,614],{"class":96},"10",[53,616,617],{"class":63},")))   ",[53,619,620],{"class":136},"# runs across 4 real cores\n",[15,622,623],{},"Inter-process communication isn't free (arguments and results are serialised), so it pays\noff when the computation per task clearly outweighs the messaging overhead.",[10,625,627],{"id":626},"picking-a-tool-with-concurrentfutures","Picking a tool with concurrent.futures",[15,629,630,633],{},[50,631,632],{},"concurrent.futures"," gives both models the same high-level API, so you can match the tool\nto the workload by swapping one class.",[43,635,637],{"className":45,"code":636,"language":47,"meta":48,"style":48},"from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor\n\n# I\u002FO-bound: threads\nwith ThreadPoolExecutor(max_workers=8) as ex:\n    results = list(ex.map(fetch, urls))\n\n# CPU-bound: processes\nwith ProcessPoolExecutor(max_workers=4) as ex:\n    results = list(ex.map(square, big_numbers))\n",[50,638,639,651,655,660,683,696,700,705,724],{"__ignoreMap":48},[53,640,641,643,646,648],{"class":55,"line":56},[53,642,519],{"class":59},[53,644,645],{"class":63}," concurrent.futures ",[53,647,60],{"class":59},[53,649,650],{"class":63}," ThreadPoolExecutor, ProcessPoolExecutor\n",[53,652,653],{"class":55,"line":67},[53,654,71],{"emptyLinePlaceholder":70},[53,656,657],{"class":55,"line":74},[53,658,659],{"class":136},"# I\u002FO-bound: threads\n",[53,661,662,665,668,671,673,676,678,680],{"class":55,"line":87},[53,663,664],{"class":59},"with",[53,666,667],{"class":63}," ThreadPoolExecutor(",[53,669,670],{"class":162},"max_workers",[53,672,93],{"class":59},[53,674,675],{"class":96},"8",[53,677,593],{"class":63},[53,679,285],{"class":59},[53,681,682],{"class":63}," ex:\n",[53,684,685,688,690,693],{"class":55,"line":100},[53,686,687],{"class":63},"    results ",[53,689,93],{"class":59},[53,691,692],{"class":96}," list",[53,694,695],{"class":63},"(ex.map(fetch, urls))\n",[53,697,698],{"class":55,"line":124},[53,699,71],{"emptyLinePlaceholder":70},[53,701,702],{"class":55,"line":140},[53,703,704],{"class":136},"# CPU-bound: processes\n",[53,706,707,709,712,714,716,718,720,722],{"class":55,"line":145},[53,708,664],{"class":59},[53,710,711],{"class":63}," ProcessPoolExecutor(",[53,713,670],{"class":162},[53,715,93],{"class":59},[53,717,590],{"class":96},[53,719,593],{"class":63},[53,721,285],{"class":59},[53,723,682],{"class":63},[53,725,726,728,730,732],{"class":55,"line":151},[53,727,687],{"class":63},[53,729,93],{"class":59},[53,731,692],{"class":96},[53,733,734],{"class":63},"(ex.map(square, big_numbers))\n",[15,736,737,738,741],{},"For high-concurrency I\u002FO specifically, ",[19,739,740],{},"asyncio"," is a third option — single-threaded\ncooperative concurrency that scales to thousands of connections without thread overhead.",[10,743,745],{"id":744},"recap","Recap",[15,747,29,748,750,751,754,755,757,758,760,761,763,764,766,767,769,770,773],{},[19,749,32],{}," lets only one thread run Python bytecode at a time, which is why threads give\n",[19,752,753],{},"no speedup for CPU-bound"," code — but the lock is ",[19,756,239],{},", so\nthreads shine for ",[19,759,247],{}," work. The GIL doesn't make code thread-safe: guard shared\nstate with a ",[50,762,487],{},". For true multi-core parallelism use ",[19,765,501],{}," (separate\ninterpreters, at the cost of pickling), and lean on ",[50,768,632],{}," to switch between\nthread and process pools with one line. ",[19,771,772],{},"Threads for I\u002FO, processes for CPU, async for\nmassive I\u002FO concurrency"," — and the GIL stops being mysterious.",[775,776,777],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":48,"searchDepth":67,"depth":67,"links":779},[780,781,782,783,785,786,787,788],{"id":12,"depth":67,"text":13},{"id":25,"depth":67,"text":26},{"id":194,"depth":67,"text":195},{"id":227,"depth":67,"text":784},"Where threads do help: I\u002FO-bound work",{"id":372,"depth":67,"text":373},{"id":495,"depth":67,"text":496},{"id":626,"depth":67,"text":627},{"id":744,"depth":67,"text":745},"What the Global Interpreter Lock is, why threads don't speed up CPU-bound Python but help I\u002FO-bound work, race conditions and locks, and when to reach for multiprocessing.","hard","md","Python",{},"\u002Fblog\u002Fpython-gil-threading-explained","\u002Fpython\u002Fconcurrency\u002Fgil",{"title":5,"description":789},"blog\u002Fpython-gil-threading-explained","Threading & the GIL","Concurrency & Parallelism","concurrency","2026-06-19","5rFZOnE3S28K5PSD1arScgGTBgOA0lFn8fbr679xBCk",1781808673081]