The Story That Explains the GIL 🎤GIL समझाने वाली कहानी 🎤一个讲清 GIL 的故事 🎤
The mic moves so fast that the audience thinks all 8 are talking at once. In reality only one is ever speaking. Adding more speakers does not help — the microphone is the bottleneck.
That single microphone is Python's Global Interpreter Lock (GIL). The 8 speakers are your threads. No matter how many CPU cores you have, only one thread runs Python bytecode at a time (in the standard CPython build).
Mic इतनी तेज़ी से घूमता है कि दर्शकों को लगता है सभी 8 एक साथ बोल रहे हैं। असल में हमेशा एक ही बोल रहा होता है। और वक्ता जोड़ने से कुछ नहीं होता — microphone ही रुकावट है।
वही एक microphone Python का Global Interpreter Lock (GIL) है। 8 वक्ता आपके threads हैं। आपके पास कितने भी CPU cores हों, एक समय पर सिर्फ़ एक thread Python bytecode चलाता है (standard CPython build में)।
麦克风传得太快,观众以为 8 个人在同时说话。实际上永远只有一个人在讲。增加发言者也没用 — 麦克风才是瓶颈。
这支唯一的麦克风就是 Python 的 全局解释器锁(GIL)。8 位发言者就是你的线程。无论你有多少个 CPU 核心,同一时刻只有一个线程在执行 Python 字节码(在标准的 CPython 版本中)。
The GIL is a mutex (mutual exclusion lock) inside CPython. It stops several threads from running Python bytecode at the same time. This keeps CPython simple and fast for single-threaded programs, but it limits CPU-heavy multithreaded programs.
GIL CPython के अंदर एक mutex (mutual exclusion lock) है। यह कई threads को एक ही समय पर Python bytecode चलाने से रोकता है। इससे CPython single-threaded programs के लिए सरल और तेज़ रहता है, पर CPU-भारी multithreaded programs सीमित हो जाते हैं।
GIL 是 CPython 内部的一个 互斥锁(mutex)。它阻止多个线程同时执行 Python 字节码。这让 CPython 在单线程程序中保持简单、快速,但限制了 CPU 密集型的多线程程序。
Python threads are real OS threads, but the GIL lets only one of them run Python code at a time. Threads are still great for I/O-bound work (network, disk, database) because the GIL is released while a thread waits. They do not speed up CPU-bound work — for that, use processes.
Python threads असली OS threads हैं, पर GIL एक समय पर उनमें से सिर्फ़ एक को Python code चलाने देता है। I/O-bound काम (network, disk, database) के लिए threads फिर भी बढ़िया हैं, क्योंकि thread के इंतज़ार के दौरान GIL छूट जाता है। CPU-bound काम को ये तेज़ नहीं करते — उसके लिए processes लीजिए।
Python 线程是真正的操作系统线程,但 GIL 同一时刻只允许其中一个运行 Python 代码。线程仍然非常适合 I/O 密集型 工作(网络、磁盘、数据库),因为线程等待时会释放 GIL。它们无法加速 CPU 密集型 工作 — 那种情况请使用进程。
Look for the interactive boxes: the GIL Timeline (Section 05), the Speed Lab (07), the Queue Playground (16), the PriorityQueue Lab (19), the Pipeline Lab (21) and the Bug Doctor (23). All their data comes from real Python runs.
Interactive डिब्बे खोजें: GIL Timeline (Section 05), Speed Lab (07), Queue Playground (16), PriorityQueue Lab (19), Pipeline Lab (21) और Bug Doctor (23)। इनका सारा data असली Python runs से है।
请留意这些互动框:GIL 时间线(第 05 节)、速度实验室(07)、Queue 游乐场(16)、PriorityQueue 实验室(19)、流水线实验室(21)和 Bug 医生(23)。它们的数据全部来自真实的 Python 运行。
What Exactly Is the GIL? 🔒GIL असल में क्या है? 🔒GIL 到底是什么?🔒
The Global Interpreter Lock is one process-wide lock inside the CPython interpreter. A thread must hold the GIL before it can run any Python bytecode. While one thread holds it, the others wait.
Global Interpreter Lock CPython interpreter के अंदर पूरे process का एक lock है। कोई भी Python bytecode चलाने से पहले thread के पास GIL होना चाहिए। जब एक thread इसे रखे है, बाक़ी इंतज़ार करते हैं।
全局解释器锁是 CPython 解释器中整个进程共用的一把锁。线程必须持有 GIL 才能执行任何 Python 字节码。一个线程持有它时,其他线程只能等待。
Every thread must pass through the one GIL to run Python code. However many cores exist, only one thread runs Python bytecode at a time.
Python code चलाने के लिए हर thread को एक ही GIL से गुज़रना होता है। कितने भी cores हों, एक समय पर सिर्फ़ एक thread Python bytecode चलाता है।
每个线程都必须经过唯一的 GIL 才能运行 Python 代码。无论有多少核心,同一时刻只有一个线程在执行 Python 字节码。
sleep) — or until another thread has waited for the switch interval (5 ms by default since Python 3.2) and asks it to let go.sleep) शुरू न करे — या जब तक कोई दूसरा thread switch interval (Python 3.2 से default 5 ms) तक इंतज़ार करके उसे छोड़ने को न कहे।sleep)— 或者另一个线程已经等待了一个 切换间隔(自 Python 3.2 起默认 5 毫秒)并要求它让出。You can ask Python about its GIL and switch interval directly:
आप Python से उसके GIL और switch interval के बारे में सीधे पूछ सकते हैं:
你可以直接向 Python 查询它的 GIL 和切换间隔:
import threading, sys
print("GIL enabled? ", sys._is_gil_enabled()) # Python 3.13+
print("switch interval: ", sys.getswitchinterval(), "s")
print("threads running: ", threading.active_count())
The GIL does not make Python single-threaded. You can create many threads and they run concurrently. The GIL only stops them from running Python bytecode in parallel. While a thread waits on I/O or sleeps — or runs C code that releases the GIL (many NumPy operations, file and socket calls inside libraries like requests) — other threads make real progress.
GIL Python को single-threaded नहीं बनाता। आप कई threads बना सकते हैं और वे concurrently चलते हैं। GIL सिर्फ़ उन्हें Python bytecode parallel में चलाने से रोकता है। जब कोई thread I/O का इंतज़ार करता है या sleep करता है — या GIL छोड़ने वाला C code चलाता है (कई NumPy operations, requests जैसी libraries के अंदर file और socket calls) — तब दूसरे threads सच में आगे बढ़ते हैं।
GIL 不会 让 Python 变成单线程。你可以创建很多线程,它们会 并发 运行。GIL 只是阻止它们 并行 执行 Python 字节码。当一个线程在等待 I/O、在 sleep,或者在运行会释放 GIL 的 C 代码时(许多 NumPy 操作、requests 等库内部的文件和套接字调用),其他线程可以真正向前推进。
Why Does the GIL Exist? 🤔GIL क्यों है? 🤔为什么会有 GIL?🤔
The GIL was added in the early 1990s to solve one problem: memory-management safety. CPython frees objects using reference counting. Without a global lock, two threads could change an object's reference count at the same moment, corrupt memory and crash the interpreter.
GIL 1990 के दशक की शुरुआत में एक समस्या हल करने के लिए जोड़ा गया: memory-management की सुरक्षा। CPython objects को reference counting से मुक्त करता है। Global lock के बिना दो threads एक ही पल में किसी object का reference count बदल सकते थे, memory बिगाड़ सकते थे और interpreter crash कर सकते थे।
GIL 在 1990 年代初被加入,为了解决一个问题:内存管理的安全。CPython 用 引用计数 来释放对象。没有全局锁的话,两个线程可能同时修改一个对象的引用计数,破坏内存并让解释器崩溃。
The original said free-threading (no GIL) is an experimental 3.13 build. That was true for 3.13. Since Python 3.14 (October 2025) the free-threaded build is officially supported (PEP 779). It is still a separate, opt-in build (python3.14t), not the default, and single-threaded code runs somewhat slower on it. So the advice stays the same: for normal CPython, assume the GIL exists and design for it.
मूल में कहा गया कि free-threading (बिना GIL) एक experimental 3.13 build है। यह 3.13 के लिए सही था। Python 3.14 (अक्टूबर 2025) से free-threaded build आधिकारिक रूप से supported है (PEP 779)। यह अब भी अलग, opt-in build (python3.14t) है, default नहीं, और उस पर single-threaded code कुछ धीमा चलता है। इसलिए सलाह वही है: सामान्य CPython के लिए मानिए कि GIL है और उसी हिसाब से design कीजिए।
原文说自由线程(无 GIL)是 3.13 的实验性版本。这对 3.13 来说没错。从 Python 3.14(2025 年 10 月)起,自由线程版本已获得 官方支持(PEP 779)。但它仍是 单独的、需主动选择的版本(python3.14t),不是默认版本,而且单线程代码在上面会慢一些。所以建议不变:对普通 CPython,就当 GIL 存在来设计。
Behind the Scenes — Threads and PyThreadState 🛠️पर्दे के पीछे — Threads और PyThreadState 🛠️幕后 — 线程与 PyThreadState 🛠️
When you call threading.Thread(target=…).start(), Python does surprisingly little. It creates a C structure called PyThreadState, starts a native OS thread (a POSIX thread on Linux/macOS, a Windows thread on Windows) and hands it your function. The operating system decides which core runs the thread and when — Python has no thread scheduler of its own. The GIL only decides which thread may run Python code.
जब आप threading.Thread(target=…).start() बुलाते हैं, Python हैरानी की हद तक कम करता है। यह PyThreadState नाम की एक C structure बनाता है, एक native OS thread शुरू करता है (Linux/macOS पर POSIX thread, Windows पर Windows thread) और उसे आपका function दे देता है। Thread कौन-सा core और कब चलाए, यह operating system तय करता है — Python का अपना कोई thread scheduler नहीं है। GIL सिर्फ़ तय करता है कि कौन-सा thread Python code चला सकता है।
当你调用 threading.Thread(target=…).start() 时,Python 做的事情少得惊人。它创建一个名为 PyThreadState 的 C 结构体,启动一个原生操作系统线程(Linux/macOS 上是 POSIX 线程,Windows 上是 Windows 线程),并把你的函数交给它。操作系统 决定线程何时在哪个核心上运行 — Python 自己没有线程调度器。GIL 只决定哪个线程可以运行 Python 代码。
PyThreadState struct that holds this thread's interpreter state.PyThreadState struct बनाता है जो इस thread की interpreter state रखता है।PyThreadState 结构体,保存这个线程的解释器状态。pthread_create on Unix).pthread_create)।pthread_create)。Proof that they are real OS threads — each one gets its own operating-system thread id:
सबूत कि ये असली OS threads हैं — हर एक को अपना operating-system thread id मिलता है:
证明它们是真正的操作系统线程 — 每个线程都有自己的操作系统线程 ID:
import threading
def show():
t = threading.current_thread()
print(f"{t.name:10} Python ident={t.ident % 100000:>5} OS thread id={threading.get_native_id()}")
show() # the main thread
ts = [threading.Thread(target=show) for _ in range(3)]
for t in ts: t.start()
for t in ts: t.join()
The PyThreadState structurePyThreadState structurePyThreadState 结构体
Each Python thread carries its own state: the current stack frame, recursion depth, the exception being handled, tracing hooks and a thread-local dictionary. Here is a simplified sketch based on older CPython sources:
हर Python thread अपनी state रखता है: current stack frame, recursion depth, handle हो रहा exception, tracing hooks और एक thread-local dictionary। यह पुराने CPython source पर आधारित एक सरल ढाँचा है:
每个 Python 线程都带着自己的状态:当前栈帧、递归深度、正在处理的异常、跟踪钩子和一个线程局部字典。下面是基于旧版 CPython 源码的 简化示意:
/* Include/pystate.h - simplified sketch, older CPython (2.x / early 3.x) */
typedef struct _ts {
struct _ts *next;
PyInterpreterState *interp;
struct _frame *frame; // current stack frame
int recursion_depth;
int tracing;
Py_tracefunc c_profilefunc;
Py_tracefunc c_tracefunc;
PyObject *curexc_type; // exception being raised
PyObject *curexc_value;
PyObject *curexc_traceback;
PyObject *dict; // thread-local storage
int tick_counter; // Python 2 "check interval" ticks
int gilstate_counter;
PyObject *async_exc;
long thread_id;
} PyThreadState;
/* older CPython: points to the state of the thread holding the GIL */
PyThreadState *_PyThreadState_Current = NULL;
The original said the struct is "< 100 bytes". Even the old version above has 16 fields, mostly 8-byte pointers — well over 100 bytes — and modern versions are much larger. Also, the original named PyEval_CallObject as the function that runs your callable: that C API was deprecated in 3.9 and is gone in 3.13. Fields like tick_counter and the global _PyThreadState_Current come from older CPython. The idea is still right: every thread has its own interpreter state, and one of them is "current" — the one holding the GIL.
मूल में कहा गया कि struct "< 100 bytes" है। ऊपर वाले पुराने version में भी 16 fields हैं, ज़्यादातर 8-byte pointers — 100 bytes से काफ़ी ज़्यादा — और आधुनिक versions कहीं बड़े हैं। मूल ने आपका callable चलाने वाला function PyEval_CallObject बताया: वह C API 3.9 में deprecated हुआ और 3.13 में हटा दिया गया। tick_counter और global _PyThreadState_Current जैसे fields पुराने CPython के हैं। विचार अब भी सही है: हर thread की अपनी interpreter state है, और उनमें से एक "current" है — जो GIL रखे है।
原文说这个结构体"小于 100 字节"。即使上面这个旧版本也有 16 个字段,大多是 8 字节的指针 — 远超 100 字节 — 现代版本则大得多。原文还说运行你的可调用对象的函数是 PyEval_CallObject:这个 C API 在 3.9 被弃用,在 3.13 中已被移除。tick_counter 和全局的 _PyThreadState_Current 这类字段来自旧版 CPython。但核心思想仍然正确:每个线程都有自己的解释器状态,其中一个是"当前"的 — 也就是持有 GIL 的那个。
Because Python threads are real OS threads, they carry OS costs (memory for a stack, context switches, scheduling). But for CPU-heavy Python code only one can hold the GIL, so most of that parallel power is unused. You pay for OS threads without getting parallel Python execution.
क्योंकि Python threads असली OS threads हैं, उनकी OS क़ीमत होती है (stack के लिए memory, context switches, scheduling)। पर CPU-भारी Python code के लिए सिर्फ़ एक GIL रख सकता है, इसलिए वह parallel ताक़त ज़्यादातर बेकार जाती है। आप OS threads की क़ीमत चुकाते हैं, पर parallel Python execution नहीं पाते।
因为 Python 线程是真正的操作系统线程,它们有操作系统层面的开销(栈内存、上下文切换、调度)。但对 CPU 密集的 Python 代码,只有一个线程能持有 GIL,所以大部分并行能力被浪费了。你付出了操作系统线程的代价,却得不到并行的 Python 执行。
Ticks, Check Intervals and the Modern Switch ⏲️Ticks, Check Intervals और आधुनिक Switch ⏲️Tick、检查间隔与现代切换机制 ⏲️
How does Python take the GIL away from a CPU-heavy thread that never does I/O? It checks from time to time. The way it checks changed a lot between Python 2 and Python 3.2.
जो CPU-भारी thread कभी I/O नहीं करता, उससे Python GIL कैसे छीनता है? यह बीच-बीच में जाँच करता है। जाँच का तरीक़ा Python 2 और Python 3.2 के बीच बहुत बदला।
Python 如何从一个从不做 I/O 的 CPU 密集线程手中拿走 GIL?它会时不时地检查一下。检查的方式在 Python 2 和 Python 3.2 之间变化很大。
| Propertyगुण特性 | Valueमान值 |
|---|---|
| TriggerTrigger触发 | Every 100 bytecode "ticks"हर 100 bytecode "ticks"每 100 个字节码"tick" |
| Time-based?समय पर आधारित?基于时间? | No — count-basedनहीं — गिनती पर否 — 基于计数 |
| SettingSetting设置 | sys.setcheckinterval() |
| Problemसमस्या问题 | One tick can hide a long C callएक tick में लंबी C call छिप सकती है一个 tick 可能藏着很长的 C 调用 |
| MulticoreMulticore多核 | Very bad — the "GIL battle"बहुत ख़राब — "GIL battle"非常糟 —"GIL 争夺战" |
| Propertyगुण特性 | Valueमान值 |
|---|---|
| TriggerTrigger触发 | A waiting thread waits 5 ms, then asks for the GILइंतज़ार करता thread 5 ms रुककर GIL माँगता है等待中的线程等 5 毫秒后请求 GIL |
| Time-based?समय पर आधारित?基于时间? | Yes — real millisecondsहाँ — असली milliseconds是 — 真实的毫秒 |
| SettingSetting设置 | sys.setswitchinterval(0.005) |
| Problemसमस्या问题 | I/O threads can wait up to one intervalI/O threads एक interval तक इंतज़ार कर सकते हैंI/O 线程最多可能等一个间隔 |
| MulticoreMulticore多核 | Fair hand-off, still no parallel Pythonनिष्पक्ष hand-off, फिर भी parallel Python नहीं公平交接,但仍无并行 Python |
Python 2 counted bytecodes, so one slow C call could block every other thread. Python 3.2+ uses wall-clock time and gives even turns.
Python 2 bytecodes गिनता था, इसलिए एक धीमी C call बाक़ी हर thread को रोक सकती थी। Python 3.2+ असली समय इस्तेमाल करता है और बराबर बारी देता है।
Python 2 按字节码计数,一个慢的 C 调用就能挡住所有其他线程。Python 3.2+ 使用真实时间,轮换更均匀。
What is a "tick"?"Tick" क्या है?什么是"tick"?
In Python 2 a tick was roughly one virtual-machine instruction. You can see the instructions of any function with the dis module. Here is the real output on Python 3.13.16:
Python 2 में एक tick मोटे तौर पर एक virtual-machine instruction था। किसी भी function के instructions dis module से देख सकते हैं। यह Python 3.13.16 का असली output है:
在 Python 2 中,一个 tick 大致对应一条虚拟机指令。你可以用 dis 模块查看任意函数的指令。下面是 Python 3.13.16 上的真实输出:
import dis
def countdown(n):
while n > 0:
print(n)
n -= 1
dis.dis(countdown)
The original listing had SETUP_LOOP, PRINT_ITEM and PRINT_NEWLINE — instructions that only exist in Python 2 (where print was a statement). The real Python 3.13.16 output above uses RESUME, CALL, BINARY_OP and JUMP_BACKWARD. Also, sys.setcheckinterval() no longer exists — it was removed in Python 3.9. Use sys.setswitchinterval().
मूल listing में SETUP_LOOP, PRINT_ITEM और PRINT_NEWLINE थे — ये instructions सिर्फ़ Python 2 में हैं (जहाँ print एक statement था)। ऊपर का असली Python 3.13.16 output RESUME, CALL, BINARY_OP और JUMP_BACKWARD इस्तेमाल करता है। साथ ही sys.setcheckinterval() अब मौजूद नहीं — इसे Python 3.9 में हटा दिया गया। sys.setswitchinterval() लीजिए।
原文列出的 SETUP_LOOP、PRINT_ITEM 和 PRINT_NEWLINE 只存在于 Python 2(那时 print 是语句)。上面 Python 3.13.16 的真实输出使用的是 RESUME、CALL、BINARY_OP 和 JUMP_BACKWARD。另外,sys.setcheckinterval() 已不存在 — 它在 Python 3.9 中被移除。请使用 sys.setswitchinterval()。
One tick could hide a huge amount of work if it called a C function. In Python 2, -1 in range(100000000) was one tick that could take seconds — no other thread ran, and even Ctrl-C was ignored. Python 3.2's time-based switch interval largely fixed this.
अगर कोई tick C function बुलाता, तो उसमें बहुत सारा काम छिप सकता था। Python 2 में -1 in range(100000000) एक tick था जो seconds ले सकता था — कोई और thread नहीं चलता, और Ctrl-C भी अनदेखा होता। Python 3.2 के time-based switch interval ने इसे काफ़ी हद तक ठीक किया।
如果一个 tick 调用了 C 函数,它可能藏着大量工作。在 Python 2 中,-1 in range(100000000) 只是一个 tick,却可能耗时数秒 — 期间其他线程都无法运行,甚至 Ctrl-C 也被忽略。Python 3.2 基于时间的切换间隔基本解决了这个问题。
The check itself — Python 2 ceval.c (simplified)जाँच ख़ुद — Python 2 ceval.c (सरल)检查本身 — Python 2 的 ceval.c(简化)
/* Python/ceval.c - Python 2 era, simplified */
if (--_Py_Ticker < 0) {
_Py_Ticker = _Py_CheckInterval; /* reset: 100 ticks */
if (things_to_do) {
if (Py_MakePendingCalls() < 0) { ... } /* e.g. signal handlers */
}
if (interpreter_lock) {
/* Give another thread a chance */
PyThread_release_lock(interpreter_lock);
/* Other threads may run now */
PyThread_acquire_lock(interpreter_lock, 1);
}
}
Look at the last two lines: release, then immediately acquire again. On a multicore machine the releasing thread often wins the race back — the root of the "GIL battle" in Section 09. Python 3.2 replaced this code.
आख़िरी दो lines देखिए: release, फिर तुरंत फिर से acquire। Multicore machine पर छोड़ने वाला thread अक्सर race फिर जीत जाता है — Section 09 के "GIL battle" की जड़ यही है। Python 3.2 ने यह code बदल दिया।
看最后两行:释放,然后 立刻 再次获取。在多核机器上,释放锁的线程常常又抢回了锁 — 这就是第 09 节"GIL 争夺战"的根源。Python 3.2 替换了这段代码。
⏱️ Simulator: GIL Timeline (real traces)⏱️ Simulator: GIL Timeline (असली traces)⏱️ 模拟器:GIL 时间线(真实追踪)
Each picture is a real 60 ms recording on Python 3.13.16. Every thread logged when it was running Python code. The top lane shows who held the GIL. Red blocks are an I/O thread that has finished waiting but still has no GIL. Change the workload and the switch interval.
हर तस्वीर Python 3.13.16 पर असली 60 ms की recording है। हर thread ने दर्ज किया कि कब वह Python code चला रहा था। सबसे ऊपर की lane दिखाती है कि GIL किसके पास था। लाल blocks वह I/O thread हैं जिसका इंतज़ार ख़त्म हो गया पर GIL अभी नहीं मिला। Workload और switch interval बदलिए।
每张图都是 Python 3.13.16 上 真实的 60 毫秒记录。每个线程都记录了自己何时在运行 Python 代码。最上面一行显示 GIL 在谁手里。红色块表示 I/O 线程已经等完了,却还没拿到 GIL。试着切换工作负载和切换间隔。
GIL Behaviour — Releasing It for I/O 🔄GIL का व्यवहार — I/O के लिए छोड़ना 🔄GIL 的行为 — 为 I/O 释放 🔄
For I/O-bound threads the rule is simple: a thread holds the GIL while it runs Python code, and releases it whenever it blocks for I/O. Another ready thread takes the GIL and runs.
I/O-bound threads के लिए नियम सरल है: thread Python code चलाते समय GIL रखता है, और I/O के लिए रुकते ही उसे छोड़ देता है। कोई दूसरा तैयार thread GIL लेकर चलता है।
对 I/O 密集型线程,规则很简单:线程运行 Python 代码时持有 GIL,一旦因 I/O 阻塞就释放它。另一个就绪的线程拿到 GIL 并运行。
While T1 waits on I/O it releases the GIL, and T2 runs its Python code. This is why threads work well for I/O-heavy programs.
जब T1 I/O का इंतज़ार करता है, वह GIL छोड़ देता है, और T2 अपना Python code चलाता है। इसीलिए threads I/O-भारी programs में अच्छे चलते हैं।
T1 等待 I/O 时释放 GIL,T2 运行它的 Python 代码。这就是线程适合 I/O 密集程序的原因。
socket.recv(), file.read() or time.sleep() release the GIL just before they block.socket.recv(), file.read() या time.sleep() जैसी calls रुकने से ठीक पहले GIL छोड़ देती हैं।socket.recv()、file.read() 或 time.sleep() 这类调用会在阻塞前释放 GIL。Network and disk operations are often thousands of times slower than CPU work, so in I/O-heavy programs the GIL is free most of the time. That is why threads plus a Queue remain a great pattern for downloading, file processing and talking to many servers. Try "2 I/O threads" in the GIL Timeline: the GIL is idle most of the time.
Network और disk operations अक्सर CPU काम से हज़ारों गुना धीमे होते हैं, इसलिए I/O-भारी programs में GIL ज़्यादातर समय ख़ाली रहता है। इसीलिए threads के साथ Queue downloading, file processing और कई servers से बात करने के लिए बढ़िया pattern है। GIL Timeline में "2 I/O threads" आज़माइए: GIL ज़्यादातर समय ख़ाली है।
网络和磁盘操作通常比 CPU 工作慢上千倍,所以在 I/O 密集的程序里,GIL 大部分时间是空闲的。这就是为什么线程加 Queue 仍然是下载、文件处理和与多台服务器通信的好模式。在 GIL 时间线中试试"2 个 I/O 线程":GIL 大部分时间都空着。
CPU-Bound vs I/O-Bound — When the GIL Bites 📈CPU-Bound बनाम I/O-Bound — GIL कब काटता है 📈CPU 密集 vs I/O 密集 — GIL 何时"咬人" 📈
Real numbers from Sections 13 and 14. The same threading code gives opposite results — the type of work decides.
Sections 13 और 14 के असली numbers। एक ही threading code उलटे नतीजे देता है — काम का प्रकार तय करता है।
来自第 13、14 节的真实数据。同样的线程代码得到相反的结果 — 由工作类型决定。
| WorkloadWorkload工作负载 | Threads help?Threads मदद करें?线程有帮助吗? |
|---|---|
| Prime number sievePrime number sieve素数筛 | Noनहीं否 |
| Image processing (pure Python)Image processing (pure Python)图像处理(纯 Python) | Noनहीं否 |
| Matrix multiply (pure Python)Matrix multiply (pure Python)矩阵乘法(纯 Python) | Noनहीं否 |
| Sorting big in-memory listsबड़ी in-memory lists sort करना对内存中的大列表排序 | Noनहीं否 |
| Parsing millions of JSON recordsलाखों JSON records parse करना解析数百万条 JSON 记录 | Noनहीं否 |
| Solutionहल方案 | multiprocessing |
| WorkloadWorkload工作负载 | Threads help?Threads मदद करें?线程有帮助吗? |
|---|---|
| HTTP requests to 100 URLs100 URLs को HTTP requests请求 100 个 URL | Yes — often 10× or moreहाँ — अक्सर 10× या ज़्यादा是 — 常达 10 倍以上 |
| Reading many filesकई files पढ़ना读取大量文件 | Yesहाँ是 |
| Database queriesDatabase queries数据库查询 | Yesहाँ是 |
| Waiting for user inputUser input का इंतज़ार等待用户输入 | Yesहाँ是 |
| Copying data over a networkNetwork पर data copy करना通过网络复制数据 | Yesहाँ是 |
| Solutionहल方案 | threading + Queue |
🏁 Simulator: Speed Lab (real benchmarks)🏁 Simulator: Speed Lab (असली benchmarks)🏁 模拟器:速度实验室(真实基准测试)
The same amount of work, done three ways on our 2-core machine: one after another, with threads, and with processes. Every bar is the middle of three real runs.
उतना ही काम, हमारी 2-core machine पर तीन तरह से: एक के बाद एक, threads से, और processes से। हर bar तीन असली runs का बीच वाला मान है।
同样的工作量,在我们的双核机器上用三种方式完成:依次执行、用线程、用进程。每个条形都是三次真实运行的中位数。
If your program spends its time waiting (network, disk, database) → threads win. If it spends its time computing in Python → processes win. Not sure? Measure first: compare time.perf_counter() (wall time) with time.process_time() (CPU time). If CPU time is close to wall time, you are CPU-bound.
अगर आपका program अपना समय इंतज़ार में बिताता है (network, disk, database) → threads जीतते हैं। अगर Python में गणना में बिताता है → processes जीतते हैं। पक्का नहीं? पहले मापिए: time.perf_counter() (असली समय) की तुलना time.process_time() (CPU समय) से कीजिए। अगर CPU समय असली समय के क़रीब है, तो आप CPU-bound हैं।
如果程序把时间花在 等待 上(网络、磁盘、数据库)→ 线程胜出。如果把时间花在 Python 的 计算 上 → 进程胜出。拿不准?先测量:比较 time.perf_counter()(实际时间)和 time.process_time()(CPU 时间)。如果 CPU 时间接近实际时间,你就是 CPU 密集型。
The Beazley Experiment — When 2 Threads Were Slower Than 1 🐢Beazley Experiment — जब 2 Threads 1 से धीमे थे 🐢Beazley 实验 — 当 2 个线程比 1 个还慢 🐢
Then he turned off one CPU core, and the threaded version dropped to 38 s. It was faster on one core than on two!
His talk "Inside the Python GIL" became one of the most famous Python performance talks. It showed that the old GIL was not just useless on multicore machines — it could actively hurt.
फिर उन्होंने एक CPU core बंद किया, और threaded version 38 s पर आ गया। एक core पर यह दो से तेज़ था!
उनकी talk "Inside the Python GIL" सबसे मशहूर Python performance talks में से एक बनी। इसने दिखाया कि पुराना GIL multicore machines पर सिर्फ़ बेकार नहीं था — वह नुक़सान भी कर सकता था।
然后他关掉了一个 CPU 核心,多线程版本降到了 38 秒。单核居然比双核还快!
他的演讲《Inside the Python GIL》成为最著名的 Python 性能演讲之一。它表明旧的 GIL 在多核机器上不只是没用 — 甚至会帮倒忙。
The famous result: on the old GIL, a second core made threaded Python slower, because the two cores fought over the lock.
मशहूर नतीजा: पुराने GIL पर दूसरे core ने threaded Python को धीमा कर दिया, क्योंकि दोनों cores lock के लिए लड़ते थे।
著名的结果:在旧 GIL 上,第二个核心反而让多线程 Python 变慢,因为两个核心在争抢这把锁。
Re-running the experiment todayआज यह experiment फिर चलाना今天重新运行这个实验
import time
from threading import Thread
def count(n):
while n > 0:
n -= 1
COUNT = 20_000_000 # Beazley used 100_000_000
# --- Sequential: count twice, one after the other ---
start = time.perf_counter()
count(COUNT)
count(COUNT)
print(f"Sequential : {time.perf_counter() - start:.2f}s")
# --- Two threads: each thread counts the full COUNT ---
start = time.perf_counter()
t1 = Thread(target=count, args=(COUNT,))
t2 = Thread(target=count, args=(COUNT,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Threaded : {time.perf_counter() - start:.2f}s")
The original presented Beazley's numbers as how Python behaves. They were measured on Python 2.6. Python 3.2 (2010) introduced a new GIL that hands the lock over fairly, so the multicore fight that caused the slowdown mostly disappeared. Our real run on Python 3.13.16 (2 cores, smaller COUNT) shows threaded ≈ sequential (1.08 s vs 1.11 s). The lesson that remains: threads give no speed-up for CPU-bound Python code — but they no longer make it dramatically slower.
मूल ने Beazley के numbers को Python के व्यवहार की तरह दिखाया। वे Python 2.6 पर मापे गए थे। Python 3.2 (2010) ने एक नया GIL लाया जो lock निष्पक्ष तरीक़े से सौंपता है, इसलिए धीमेपन की वजह वाली multicore लड़ाई ज़्यादातर ख़त्म हो गई। Python 3.13.16 पर हमारा असली run (2 cores, छोटा COUNT) दिखाता है threaded ≈ sequential (1.08 s बनाम 1.11 s)। जो सबक़ बचा है: CPU-bound Python code में threads से कोई speed-up नहीं मिलता — पर अब वे इसे बहुत धीमा भी नहीं करते।
原文把 Beazley 的数据当作 Python 的现状来展示。那些数据是在 Python 2.6 上测得的。Python 3.2(2010 年)引入了 新的 GIL,它会公平地交接锁,所以导致变慢的多核争夺基本消失了。我们在 Python 3.13.16 上的真实运行(2 核,较小的 COUNT)显示多线程 ≈ 顺序执行(1.08 秒对 1.11 秒)。仍然成立的教训是:线程无法加速 CPU 密集的 Python 代码 — 但它们也不再让代码大幅变慢。
With one core only one thread can run, so there is no wasted signalling. With two cores the OS runs both threads at the same time, and they enter a constant fight: release, signal, wake, fail, sleep — thousands of system calls per second spent on the lock, not on your work. The next two sections show this battle and its cost.
एक core पर सिर्फ़ एक thread चल सकता है, इसलिए signalling बर्बाद नहीं होती। दो cores पर OS दोनों threads को एक साथ चलाता है, और वे लगातार लड़ते हैं: release, signal, जागना, fail, सोना — हर second हज़ारों system calls lock पर ख़र्च, आपके काम पर नहीं। अगले दो sections यह लड़ाई और इसकी क़ीमत दिखाते हैं।
单核时只有一个线程能运行,所以没有浪费的信号。双核时操作系统让两个线程同时运行,它们陷入不停的争夺:释放、发信号、唤醒、失败、睡眠 — 每秒数千次系统调用都花在了锁上,而不是你的工作上。接下来两节展示这场争夺及其代价。
The Multicore GIL Battle (Old GIL) ⚔️Multicore GIL Battle (पुराना GIL) ⚔️多核 GIL 争夺战(旧 GIL)⚔️
On a single core the OS runs one thread at a time, so the GIL is barely contested. On several cores the OS happily runs threads on different cores at the same moment, and with the old (Python 2) GIL they started fighting over the one lock.
एक core पर OS एक समय में एक thread चलाता है, इसलिए GIL के लिए मुश्किल से होड़ होती है। कई cores पर OS ख़ुशी-ख़ुशी threads को एक ही पल में अलग cores पर चलाता है, और पुराने (Python 2) GIL के साथ वे एक lock के लिए लड़ने लगते थे।
在单核上,操作系统一次只运行一个线程,所以 GIL 几乎没有竞争。在多核上,操作系统乐于把线程同时放到不同核心上运行,而在 旧(Python 2)GIL 下,它们就开始争抢这唯一的锁。
With the old GIL, T1 released and grabbed the lock again before T2 on the other core could even wake up.
पुराने GIL में T1 lock छोड़कर फिर से पकड़ लेता था, इससे पहले कि दूसरे core पर T2 जाग भी पाए।
在旧 GIL 下,T1 释放锁后又立刻抢回,另一个核心上的 T2 甚至还没来得及醒来。
Beazley's instrumented trace (Python 2)Beazley का instrumented trace (Python 2)Beazley 的插桩追踪(Python 2)
# t1, t2 = thread ids. Middle number = ticks left. Right = total checks.
t2 100 5392 ENTRY
t2 100 5392 ACQUIRE
t2 100 5393 RELEASE
t1 100 5393 ACQUIRE # thread switch - t1 wins
t2 100 5393 ENTRY
t2 27 5393 BUSY # t2 tries to run but the GIL is taken by t1
t1 100 5394 RELEASE
t1 100 5394 ENTRY # t1 releases and IMMEDIATELY re-enters
t1 100 5394 ACQUIRE # t1 re-acquires before t2 can wake
t2 74 5394 RETRY # t2 wakes up, fails, goes back to sleep
t1 100 5395 RELEASE
t1 100 5395 ENTRY
t1 100 5395 ACQUIRE # t1 wins AGAIN
t2 83 5395 RETRY # t2 fails AGAIN
... # hundreds of failed retries before t2 gets a turn
The original described this battle as how the GIL works today. In Python 3.2+ the rules changed: a waiting thread waits one switch interval (5 ms) and then sets a "please drop the GIL" request. The running thread drops it and waits until another thread has actually taken it (a "forced switch"), so it can no longer grab it straight back. You can see the result in the GIL Timeline (Section 05): with "2 CPU threads" T1 and T2 take clean, even turns of about 5 ms. The fight over the lock is mostly gone — but there is still no parallel Python execution.
मूल ने इस लड़ाई को आज के GIL का व्यवहार बताया। Python 3.2+ में नियम बदले: इंतज़ार करता thread एक switch interval (5 ms) रुकता है और फिर "कृपया GIL छोड़ो" request लगाता है। चलता thread उसे छोड़ता है और तब तक रुकता है जब तक कोई दूसरा thread उसे सच में ले न ले ("forced switch"), इसलिए वह उसे तुरंत वापस नहीं झपट सकता। नतीजा GIL Timeline (Section 05) में देखिए: "2 CPU threads" में T1 और T2 लगभग 5 ms की साफ़, बराबर बारी लेते हैं। Lock की लड़ाई ज़्यादातर ख़त्म — पर parallel Python execution अब भी नहीं।
原文把这场争夺描述成今天 GIL 的工作方式。在 Python 3.2+ 中规则变了:等待中的线程先等一个切换间隔(5 毫秒),然后发出"请释放 GIL"的请求。正在运行的线程释放 GIL,并一直等到另一个线程真正拿到它("强制切换"),所以它无法立刻再抢回来。结果可以在 GIL 时间线(第 05 节)中看到:选择"2 个 CPU 线程"时,T1 和 T2 以约 5 毫秒为单位干净、均匀地轮流。对锁的争夺基本消失了 — 但仍然没有并行的 Python 执行。
Python wants only one thread to run Python code at a time, but it leaves scheduling to the OS. The OS wants to use every core and runs threads at the same time. These goals clash. The old GIL lost that clash badly; the new GIL manages it more fairly, but the clash itself is why threads cannot speed up CPU-bound Python code.
Python चाहता है कि एक समय पर सिर्फ़ एक thread Python code चलाए, पर scheduling OS पर छोड़ देता है। OS हर core इस्तेमाल करना चाहता है और threads को एक साथ चलाता है। ये लक्ष्य टकराते हैं। पुराना GIL यह टकराव बुरी तरह हारता था; नया GIL इसे ज़्यादा निष्पक्ष संभालता है, पर यही टकराव कारण है कि threads CPU-bound Python code को तेज़ नहीं कर सकते।
Python 希望同一时刻只有一个线程运行 Python 代码,却把调度交给了操作系统。操作系统想用满每个核心,会让线程同时运行。这两个目标相互冲突。旧 GIL 在这场冲突中输得很惨;新 GIL 处理得更公平,但正是这个冲突导致线程无法加速 CPU 密集的 Python 代码。
Signalling Overhead — Where the Time Went 📡Signalling Overhead — समय कहाँ गया 📡信号开销 — 时间都去哪了 📡
The GIL is not a plain mutex. On Unix it is built from a mutex plus a condition variable (older builds used a semaphore). Every hand-off involves signalling, and signalling costs system calls. Beazley counted them in 2009:
GIL एक साधारण mutex नहीं है। Unix पर यह एक mutex और एक condition variable से बना है (पुराने builds में semaphore)। हर hand-off में signalling होती है, और signalling की क़ीमत system calls है। Beazley ने 2009 में इन्हें गिना:
GIL 不是一个普通的互斥锁。在 Unix 上它由一个互斥锁加一个条件变量构成(较旧的版本用信号量)。每次交接都涉及 发信号,而发信号要付出系统调用的代价。Beazley 在 2009 年统计过:
| Scenario (Python 2.6, OS X)Scenario (Python 2.6, OS X)场景(Python 2.6,OS X) | Unix syscallsUnix syscallsUnix 系统调用 | Mach syscallsMach syscallsMach 系统调用 | Commentटिप्पणी说明 |
|---|---|---|---|
| Sequential (1 CPU)Sequential (1 CPU)顺序执行(1 CPU) | 736 | 117 | Baseline — no threading costBaseline — threading की कोई क़ीमत नहीं基线 — 没有线程开销 |
| 2 CPU-bound threads (1 CPU)2 CPU-bound threads (1 CPU)2 个 CPU 密集线程(1 CPU) | 1,149 | ~3,300,000 | 3.3 million lock signals!33 लाख lock signals!330 万次锁信号! |
| 2 CPU-bound threads (2 CPUs)2 CPU-bound threads (2 CPUs)2 个 CPU 密集线程(2 CPU) | 1,149 | ~9,500,000 | Nearly 3× worse on multicoreMulticore पर लगभग 3× बदतर多核上差了近 3 倍 |
Each hand-off locks a mutex, signals a condition variable that another thread is waiting on, makes a round trip through the kernel to wake that thread, and later takes the lock again. With a 5 ms interval that can happen about 200 times a second per busy thread. The numbers above are from the old GIL; the new GIL does far fewer pointless wake-ups, but every hand-off still costs system calls.
हर hand-off एक mutex lock करता है, उस condition variable को signal देता है जिस पर दूसरा thread इंतज़ार कर रहा है, उस thread को जगाने के लिए kernel से होकर आता है, और बाद में lock फिर लेता है। 5 ms interval के साथ यह हर व्यस्त thread पर second में लगभग 200 बार हो सकता है। ऊपर के numbers पुराने GIL के हैं; नया GIL बहुत कम बेकार wake-ups करता है, पर हर hand-off में अब भी system calls लगती हैं।
每次交接都要锁一个互斥锁,向另一个线程正在等待的条件变量发信号,经过内核往返一次去唤醒那个线程,之后再重新获取锁。在 5 毫秒间隔下,每个忙碌线程每秒可能发生约 200 次。上面的数据来自旧 GIL;新 GIL 的无效唤醒少得多,但每次交接仍要付出系统调用的代价。
Priority Inversion — When CPU Threads Slow Down I/O Threads 🚨Priority Inversion — जब CPU Threads I/O Threads को धीमा करें 🚨优先级反转 — 当 CPU 线程拖慢 I/O 线程 🚨
Now a mixed program: one thread crunches numbers, another waits on a socket. A packet arrives — the I/O thread wants to run now — but it must first get the GIL from the busy CPU thread.
अब एक मिला-जुला program: एक thread numbers crunch करता है, दूसरा socket पर इंतज़ार करता है। एक packet आता है — I/O thread अभी चलना चाहता है — पर उसे पहले व्यस्त CPU thread से GIL लेना होगा।
现在是一个混合程序:一个线程在做数值计算,另一个在套接字上等待。一个数据包到达 — I/O 线程想 立刻 运行 — 但它必须先从忙碌的 CPU 线程手里拿到 GIL。
The OS treats the I/O thread as urgent, but the GIL does not care about OS priority.
OS I/O thread को ज़रूरी मानता है, पर GIL को OS priority की परवाह नहीं।
操作系统认为 I/O 线程很紧急,但 GIL 并不在乎操作系统的优先级。
Measuring it on Python 3.13.16Python 3.13.16 पर इसे मापना在 Python 3.13.16 上测量
import threading, time, statistics
def io_worker(results):
for _ in range(100):
t0 = time.perf_counter()
time.sleep(0.001) # wait 1 ms for a "network packet"
results.append((time.perf_counter() - t0) * 1000)
stop = False
def cpu_worker():
n = 0
while not stop: # pure Python number crunching
n += 1
for busy in (False, True):
stop, results = False, []
if busy:
c = threading.Thread(target=cpu_worker); c.start()
t = threading.Thread(target=io_worker, args=(results,))
t.start(); t.join()
stop = True
if busy: c.join()
print(f"CPU thread running: {busy!s:5} "
f"avg wake-up {statistics.mean(results):.2f} ms "
f"worst {max(results):.2f} ms")
The original quoted Beazley's Python 2 trace, where an I/O thread waited through ~16,000 ticks. On Python 3 the wait is bounded by the switch interval. Our real run shows it clearly: a 1 ms sleep took about 1.1 ms alone, but about 6.2 ms while a CPU thread was busy — roughly 5 ms extra, every time. That is the "convoy effect": with 1,000 small socket reads, 5 ms each adds 5 seconds. In the GIL Timeline, pick "CPU + I/O" and compare the switch intervals: the red waiting blocks shrink with 1 ms and grow with 20 ms.
मूल ने Beazley का Python 2 trace उद्धृत किया, जहाँ एक I/O thread ~16,000 ticks तक इंतज़ार करता रहा। Python 3 पर इंतज़ार switch interval से सीमित है। हमारा असली run इसे साफ़ दिखाता है: 1 ms का sleep अकेले लगभग 1.1 ms लेता था, पर CPU thread के व्यस्त रहते लगभग 6.2 ms — हर बार लगभग 5 ms ज़्यादा। यही "convoy effect" है: 1,000 छोटे socket reads पर हर एक के 5 ms मिलकर 5 seconds। GIL Timeline में "CPU + I/O" चुनकर switch intervals की तुलना कीजिए: 1 ms पर लाल इंतज़ार के blocks छोटे होते हैं और 20 ms पर बड़े।
原文引用了 Beazley 的 Python 2 追踪,其中 I/O 线程等待了约 16,000 个 tick。在 Python 3 上,等待时间以切换间隔为上限。我们的真实运行清楚地显示了这一点:单独运行时,1 毫秒的 sleep 约耗时 1.1 毫秒;而 CPU 线程忙碌时约为 6.2 毫秒 — 每次多出约 5 毫秒。这就是"护航效应":1,000 次小的套接字读取,每次 5 毫秒就累计 5 秒。在 GIL 时间线中选择"CPU + I/O"并比较不同的切换间隔:1 毫秒时红色等待块变短,20 毫秒时变长。
A low-priority CPU thread ends up delaying a high-priority I/O thread — priority inversion caused by the interpreter lock. This is why mixing heavy CPU work with latency-sensitive I/O in one Python process is a bad idea. Move the CPU work to a separate process.
कम-ज़रूरी CPU thread ज़रूरी I/O thread को देर कराता है — interpreter lock से पैदा priority inversion। इसीलिए एक Python process में भारी CPU काम और जल्दी-जवाब वाला I/O मिलाना बुरा विचार है। CPU काम अलग process में ले जाइए।
低优先级的 CPU 线程最终拖慢了高优先级的 I/O 线程 — 这是由解释器锁导致的优先级反转。所以在同一个 Python 进程中混合繁重的 CPU 工作和对延迟敏感的 I/O 是个坏主意。把 CPU 工作移到单独的进程中。
Signals, Ctrl-C and Threaded Programs ⌨️Signals, Ctrl-C और Threaded Programs ⌨️信号、Ctrl-C 与多线程程序 ⌨️
kill -9 it.This is a classic annoyance, and it comes from how signals work with threads.
kill -9 करते हैं।यह एक पुरानी झुंझलाहट है, और यह signals के threads के साथ काम करने के तरीक़े से आती है।
kill -9 杀掉它。这是一个经典的烦恼,源于信号与线程的交互方式。
KeyboardInterrupt) only run in the main thread.KeyboardInterrupt raise करने वाला) सिर्फ़ main thread में चलते हैं।KeyboardInterrupt 的那个)只在 主线程 中运行。KeyboardInterrupt, but the program still does not exit until the workers stop.KeyboardInterrupt मिल सकता है, पर workers रुकने तक program फिर भी exit नहीं होता।KeyboardInterrupt,但在工作线程停止之前,程序仍然不会退出。What really happens — a real run with two Ctrl-C pressesअसल में क्या होता है — दो Ctrl-C के साथ असली run实际情况 — 按两次 Ctrl-C 的真实运行
import threading, time
def worker():
while True: # never stops on its own
time.sleep(0.1)
t = threading.Thread(target=worker)
t.start()
print("main: waiting on t.join() ...")
t.join() # bare join, no timeout
The original said a main thread blocked in t.join() "will never get scheduled to handle the signal", and that Python "thread-switches after every tick" after Ctrl-C. Both describe Python 2. Since Python 3.2, waiting on a lock can be interrupted by signals on POSIX systems. Our real run shows the first Ctrl-C did raise KeyboardInterrupt inside t.join() — but the program kept running, because the non-daemon worker never stopped. Only the second Ctrl-C, during the shutdown wait, ended it. (Behaviour on Windows can differ.) So the real fix is to give workers a way to stop.
मूल में कहा गया कि t.join() में रुका main thread "signal संभालने के लिए कभी schedule नहीं होगा", और Ctrl-C के बाद Python "हर tick के बाद thread-switch करता है"। दोनों Python 2 के बारे में हैं। Python 3.2 से POSIX systems पर lock का इंतज़ार signals से interrupt हो सकता है। हमारा असली run दिखाता है कि पहले Ctrl-C ने t.join() के अंदर KeyboardInterrupt raise किया — पर program चलता रहा, क्योंकि non-daemon worker कभी नहीं रुका। सिर्फ़ दूसरे Ctrl-C ने, shutdown के इंतज़ार के दौरान, उसे ख़त्म किया। (Windows पर व्यवहार अलग हो सकता है।) इसलिए असली इलाज workers को रुकने का तरीक़ा देना है।
原文说阻塞在 t.join() 中的主线程"永远不会被调度来处理信号",并且按下 Ctrl-C 后 Python"每个 tick 都切换线程"。这两点描述的都是 Python 2。从 Python 3.2 起,在 POSIX 系统上等待锁可以被信号中断。我们的真实运行显示,第一次 Ctrl-C 确实 在 t.join() 内抛出了 KeyboardInterrupt — 但程序仍在运行,因为非守护工作线程从未停止。只有第二次 Ctrl-C(在关闭等待期间)才结束了它。(Windows 上的行为可能不同。)所以真正的解决办法是给工作线程一个停止的途径。
Safe pattern — an interruptible threaded programसुरक्षित pattern — interrupt होने वाला threaded program安全模式 — 可中断的多线程程序
import threading
import time
stop_event = threading.Event()
def worker():
while not stop_event.is_set():
# do real work, but check stop_event periodically
time.sleep(0.1)
print("Worker exiting cleanly.")
t = threading.Thread(target=worker)
t.start()
try:
# Main thread stays responsive to signals
while t.is_alive():
t.join(timeout=0.5) # timeout lets main thread wake to check signals
except KeyboardInterrupt:
print("\nCtrl-C received - asking worker to stop")
stop_event.set()
t.join()
Give every long-running worker a stop signal — here a threading.Event it checks in its loop — and set it when KeyboardInterrupt arrives. Waiting with t.join(timeout=0.5) in a loop keeps the main thread waking up regularly, which also helps on platforms where a bare join() is not interruptible.
हर लंबे चलने वाले worker को एक stop signal दीजिए — यहाँ एक threading.Event जिसे वह अपने loop में जाँचता है — और KeyboardInterrupt आने पर उसे set कीजिए। Loop में t.join(timeout=0.5) से इंतज़ार करने से main thread नियमित रूप से जागता रहता है, जो उन platforms पर भी मदद करता है जहाँ bare join() interrupt नहीं होता।
给每个长期运行的工作线程一个停止信号 — 这里是一个它在循环中检查的 threading.Event — 并在收到 KeyboardInterrupt 时设置它。在循环中用 t.join(timeout=0.5) 等待,可以让主线程定期醒来,这在裸 join() 不可中断的平台上也有帮助。
Proving the GIL — A CPU-Bound Benchmark 🧪GIL साबित करना — एक CPU-Bound Benchmark 🧪证明 GIL 的存在 — CPU 密集基准测试 🧪
import time
import threading
from multiprocessing import Process
# A pure Python CPU-bound task
def count_down(n):
while n > 0:
n -= 1
COUNT = 50_000_000
if __name__ == "__main__": # needed for multiprocessing on Windows/macOS
# --- 1. Single thread (baseline) ---
start = time.perf_counter()
count_down(COUNT)
print(f"Single thread: {time.perf_counter() - start:.2f}s")
# --- 2. Two threads (splitting the work) ---
start = time.perf_counter()
t1 = threading.Thread(target=count_down, args=(COUNT // 2,))
t2 = threading.Thread(target=count_down, args=(COUNT // 2,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Two threads: {time.perf_counter() - start:.2f}s")
# --- 3. Two processes (bypasses the GIL) ---
start = time.perf_counter()
p1 = Process(target=count_down, args=(COUNT // 2,))
p2 = Process(target=count_down, args=(COUNT // 2,))
p1.start(); p2.start()
p1.join(); p2.join()
print(f"Two processes: {time.perf_counter() - start:.2f}s")
The original showed "Two threads: 2.61s ← SLOWER" next to "Single thread: 2.43s". Our real run gives 1.28 s vs 1.32 s. Over several runs, two threads were 0–20% slower than one thread — never faster, but far from the old 1.8×. Threads give no speed-up for this CPU work. Two processes took 0.70 s — about 1.8× faster, because each process has its own GIL and our machine has 2 cores. We also added if __name__ == "__main__":. Without it, the Process part fails on Windows and macOS, where child processes re-import the script.
मूल में "Single thread: 2.43s" के आगे "Two threads: 2.61s ← SLOWER" दिखाया गया था। हमारा असली run 1.28 s बनाम 1.32 s देता है। कई runs में दो threads एक thread से 0–20% धीमे रहे — कभी तेज़ नहीं, पर पुराने 1.8× से बहुत दूर। इस CPU काम में threads कोई speed-up नहीं देते। दो processes ने 0.70 s लिए — लगभग 1.8× तेज़, क्योंकि हर process का अपना GIL है और हमारी machine में 2 cores हैं। हमने if __name__ == "__main__": भी जोड़ा। इसके बिना Process वाला हिस्सा Windows और macOS पर fail होता है, जहाँ child processes script को फिर से import करते हैं।
原文在"Single thread: 2.43s"旁边显示"Two threads: 2.61s ← SLOWER"。我们的真实运行结果是 1.28 秒对 1.32 秒。多次运行中,两个线程比一个线程慢 0–20% — 从未更快,但远不到旧的 1.8 倍。对这种 CPU 工作,线程 没有任何加速。两个进程用了 0.70 秒 — 大约快 1.8 倍,因为每个进程都有自己的 GIL,而我们的机器有 2 个核心。我们还加上了 if __name__ == "__main__":。没有它,Process 部分在 Windows 和 macOS 上会失败,因为那里的子进程会重新导入脚本。
Threads Shine for I/O — Same Benchmark, Different Work 🌐Threads I/O में चमकते हैं — वही Benchmark, अलग काम 🌐线程在 I/O 上大放异彩 — 同样的测试,不同的工作 🌐
Our test machine has no internet, so the network call is replaced by fake_get(), which waits 1 second with time.sleep(1) — like a server that takes 1 s to reply. sleep releases the GIL exactly as a socket read does, so the timing behaves the same. In your own code, use requests.get(url).
हमारी test machine में internet नहीं है, इसलिए network call की जगह fake_get() है, जो time.sleep(1) से 1 second इंतज़ार करता है — जैसे जवाब देने में 1 s लेने वाला server। sleep GIL ठीक वैसे ही छोड़ता है जैसे socket read, इसलिए timing एक जैसी रहती है। अपने code में requests.get(url) लीजिए।
我们的测试机器没有互联网,所以网络调用换成了 fake_get(),它用 time.sleep(1) 等待 1 秒 — 就像一个需要 1 秒才回复的服务器。sleep 释放 GIL 的方式与套接字读取完全相同,所以计时表现一致。在你自己的代码中请使用 requests.get(url)。
import time
import threading
def fake_get(url):
"""Stands in for requests.get(url): waits 1 s like a slow server."""
time.sleep(1) # sleep releases the GIL, just like socket I/O
URLS = ["https://httpbin.org/delay/1"] * 10
def fetch(url):
fake_get(url) # real code: requests.get(url)
# --- Sequential ---
start = time.perf_counter()
for url in URLS:
fetch(url)
print(f"Sequential: {time.perf_counter() - start:.2f}s")
# --- 10 threads in parallel ---
start = time.perf_counter()
threads = [threading.Thread(target=fetch, args=(u,)) for u in URLS]
for t in threads: t.start()
for t in threads: t.join()
print(f"10 threads: {time.perf_counter() - start:.2f}s")
While each thread waits, the GIL is free, so the other threads start their own waits. All 10 waits overlap, and the total time shrinks to about the time of one call — exactly 10× faster here. This is why threads paired with a Queue are perfect for I/O work.
जब हर thread इंतज़ार करता है, GIL ख़ाली रहता है, इसलिए बाक़ी threads अपना इंतज़ार शुरू कर देते हैं। सभी 10 इंतज़ार एक साथ होते हैं, और कुल समय लगभग एक call के बराबर रह जाता है — यहाँ ठीक 10× तेज़। इसीलिए Queue के साथ threads I/O काम के लिए एकदम सही हैं।
每个线程等待时 GIL 是空闲的,所以其他线程也开始各自的等待。10 次等待全部重叠,总时间缩短到大约一次调用的时间 — 这里正好 快 10 倍。这就是为什么线程配合 Queue 非常适合 I/O 工作。
Enter the Queue — Thread-Safe Communication 📨Queue आता है — Thread-Safe बातचीत 📨队列登场 — 线程安全的通信 📨
Producers add items with put(); consumers take them with get(). All the locking happens inside the queue.
Producers put() से items डालते हैं; consumers get() से निकालते हैं। सारी locking queue के अंदर होती है।
生产者用 put() 放入元素,消费者用 get() 取出。所有加锁都在队列内部完成。
Python's
queue.Queue is that rail: a thread-safe, first-in-first-out buffer that hands work from producer threads to consumer threads without you writing a single Lock.Python का
queue.Queue वही rail है: एक thread-safe, first-in-first-out buffer जो producer threads से consumer threads तक काम पहुँचाता है, बिना आपके एक भी Lock लिखे।Python 的
queue.Queue 就是那根横杆:一个线程安全、先进先出的缓冲区,把工作从生产者线程交给消费者线程,你一行 Lock 都不用写。(priority, data). The lowest priority number comes out first. Good for schedulers where urgent work jumps the line.(priority, data) जैसे tuples हैं। सबसे छोटा priority number पहले निकलता है। Schedulers के लिए अच्छा जहाँ ज़रूरी काम कतार तोड़कर आगे जाए।(priority, data) 这样的元组。优先级数字最小的先出。适合紧急任务插队的调度器。In CPython, a single list.append() or list.pop() happens to be safe, but real code needs more: "wait until an item arrives", "check, then take", "stop when full". Doing those steps on a shared list from several threads is a race condition waiting to happen. queue.Queue uses internal locks and condition variables, so put(), get() and task_done() are always safe and can wait for you. You never write your own locking around it.
CPython में एक अकेला list.append() या list.pop() संयोग से सुरक्षित है, पर असली code को और चाहिए: "item आने तक रुको", "जाँचो, फिर लो", "भर जाए तो रुको"। ये क़दम कई threads से एक साझा list पर करना race condition को न्योता है। queue.Queue अंदर locks और condition variables इस्तेमाल करता है, इसलिए put(), get() और task_done() हमेशा सुरक्षित हैं और आपके लिए इंतज़ार कर सकते हैं। इसके चारों ओर आपको कभी अपनी locking नहीं लिखनी।
在 CPython 中,单独一次 list.append() 或 list.pop() 恰好是安全的,但真实代码需要更多:"等到有元素到来"、"先检查再取出"、"满了就停下"。多个线程在共享列表上做这些步骤,迟早会出竞态条件。queue.Queue 内部使用锁和条件变量,所以 put()、get() 和 task_done() 总是安全的,而且能替你 等待。你永远不需要在它外面自己加锁。
Queue API — The Essential Methods 🧰Queue API — ज़रूरी Methods 🧰Queue API — 必备方法 🧰
| MethodMethod方法 | What it doesक्या करता है作用 | Blocking behaviourBlocking व्यवहार阻塞行为 |
|---|---|---|
q.put(item) | Add an itemItem डालें放入一个元素 | Waits while a bounded queue is fullBounded queue भरी हो तो इंतज़ार有界队列已满时等待 |
q.get() | Remove and return the next itemअगला item निकालकर लौटाएँ取出并返回下一个元素 | Waits for ever while the queue is emptyQueue ख़ाली हो तो हमेशा इंतज़ार队列为空时一直等待 |
q.put(item, timeout=5) | Add, with a deadlineDeadline के साथ डालें带期限地放入 | Raises queue.Full if still full after 5 s5 s बाद भी भरी हो तो queue.Full5 秒后仍满则抛出 queue.Full |
q.get(timeout=5) | Take, with a deadlineDeadline के साथ लें带期限地取出 | Raises queue.Empty if still empty after 5 s5 s बाद भी ख़ाली हो तो queue.Empty5 秒后仍为空则抛出 queue.Empty |
q.put_nowait(item) | Add without waitingबिना इंतज़ार डालें不等待地放入 | Raises queue.Full at once if fullभरी हो तो तुरंत queue.Full已满则立即抛出 queue.Full |
q.get_nowait() | Take without waitingबिना इंतज़ार लें不等待地取出 | Raises queue.Empty at once if emptyख़ाली हो तो तुरंत queue.Empty为空则立即抛出 queue.Empty |
q.task_done() | Say "the item I got is fully processed"कहें "मेरा लिया item पूरी तरह हो गया"表示"我取出的元素已处理完" | Pairs with q.join(); too many calls → ValueErrorq.join() के साथ जोड़ी; ज़्यादा calls → ValueError与 q.join() 配对;调用过多 → ValueError |
q.join() | Wait until every put item got a task_done()हर डाले item का task_done() होने तक इंतज़ार等待每个放入的元素都得到 task_done() | Blocks until the unfinished count is 0Unfinished count 0 होने तक रुकता है阻塞直到未完成计数为 0 |
q.qsize() | Approximate current sizeलगभग मौजूदा size当前的大致大小 | Only an estimate — another thread may change it next momentसिर्फ़ अनुमान — अगले पल कोई और thread बदल सकता है只是估计 — 下一刻可能就被其他线程改变 |
🧪 Simulator: Queue Playground🧪 Simulator: Queue Playground🧪 模拟器:Queue 游乐场
Press buttons to call put_nowait(), get_nowait() and task_done(). Watch the items, the unfinished-task counter that q.join() waits on, and the exceptions. Every rule here matched 300 random sequences run on a real queue.Queue.
Buttons दबाकर put_nowait(), get_nowait() और task_done() बुलाइए। Items, q.join() जिसका इंतज़ार करता है वह unfinished-task counter, और exceptions देखिए। यहाँ का हर नियम असली queue.Queue पर चलाए गए 300 random sequences से मिलता है।
按按钮调用 put_nowait()、get_nowait() 和 task_done()。观察元素、q.join() 所等待的 未完成任务计数器 以及异常。这里的每条规则都与在真实 queue.Queue 上运行的 300 个随机序列一致。
Your First Producer-Consumer With Queue 🏭Queue के साथ आपका पहला Producer-Consumer 🏭你的第一个基于 Queue 的生产者-消费者 🏭
import threading
import queue
import time
import random
# A thread-safe, bounded FIFO queue holding at most 5 items
q = queue.Queue(maxsize=5)
def producer(name):
for i in range(10):
item = f"{name}-item-{i}"
q.put(item) # blocks if queue is full
print(f"[P {name}] produced {item} (queue size={q.qsize()})")
time.sleep(random.uniform(0.05, 0.2))
def consumer(name):
while True:
item = q.get() # blocks if queue is empty
if item is None: # poison pill = shutdown signal
q.task_done()
break
print(f" [C {name}] consumed {item}")
time.sleep(random.uniform(0.1, 0.3))
q.task_done() # signal completion
# Start 2 producers and 3 consumers
producers = [threading.Thread(target=producer, args=(f"P{i}",)) for i in range(2)]
consumers = [threading.Thread(target=consumer, args=(f"C{i}",)) for i in range(3)]
for t in producers + consumers:
t.start()
for t in producers: t.join() # wait for producers to finish
q.join() # wait until all produced items are processed
for _ in consumers: # one poison pill per consumer
q.put(None)
for t in consumers: t.join()
print("All work complete.")
Read it in order: two producers add 20 items in total; three consumers take them as they arrive. q.join() returns only after every item got its task_done(). Then one None per consumer tells each consumer to stop.
क्रम से पढ़िए: दो producers कुल 20 items डालते हैं; तीन consumers आते ही उन्हें ले लेते हैं। q.join() तभी लौटता है जब हर item का task_done() हो जाए। फिर हर consumer के लिए एक None उसे रुकने को कहता है।
按顺序看:两个生产者共放入 20 个元素;三个消费者一有元素就取走。只有每个元素都得到 task_done() 后,q.join() 才会返回。然后给每个消费者放一个 None,通知它停止。
Consumers loop for ever on q.get(). To stop them cleanly, put one None per consumer into the queue after the real work is done. When a consumer gets None, it leaves its loop. This is simpler and safer than trying to kill threads (Python has no way to kill a thread from outside).
Consumers q.get() पर हमेशा loop करते हैं। उन्हें साफ़ रोकने के लिए असली काम पूरा होने के बाद queue में हर consumer के लिए एक None डालिए। जब consumer को None मिलता है, वह अपना loop छोड़ देता है। यह threads को मारने की कोशिश से सरल और सुरक्षित है (Python में बाहर से thread मारने का कोई तरीक़ा नहीं)।
消费者在 q.get() 上无限循环。要干净地停止它们,在真正的工作完成后,给每个消费者往队列里放一个 None。消费者拿到 None 就跳出循环。这比试图杀死线程更简单、更安全(Python 无法从外部杀死一个线程)。
Real-World Example — A Concurrent Fetcher 🕸️असली उदाहरण — एक Concurrent Fetcher 🕸️实战示例 — 并发抓取器 🕸️
Something practical: download 50 URLs using 10 worker threads. One queue holds the URLs; workers take them, "download" them (with fake_get() from Section 14) and put results into a second queue.
कुछ काम का: 10 worker threads से 50 URLs download करना। एक queue में URLs हैं; workers उन्हें लेकर "download" करते हैं (Section 14 वाले fake_get() से) और नतीजे दूसरी queue में डालते हैं।
来点实用的:用 10 个工作线程下载 50 个 URL。一个队列存放 URL;工作线程取出它们,"下载"(使用第 14 节的 fake_get()),再把结果放进第二个队列。
import threading
import queue
import time
def fake_get(url):
"""Stands in for requests.get(url, timeout=10)."""
time.sleep(1) # sleep releases the GIL, just like socket I/O
return 200, b"x" * 308
URL_LIST = [f"https://httpbin.org/delay/1?id={i}" for i in range(50)]
NUM_WORKERS = 10
task_queue = queue.Queue()
result_queue = queue.Queue()
def worker(worker_id):
while True:
url = task_queue.get()
if url is None: # shutdown
task_queue.task_done()
break
try:
status, content = fake_get(url)
result_queue.put((url, status, len(content)))
print(f"[W{worker_id}] {url} -> {status}")
except Exception as e:
result_queue.put((url, "ERROR", str(e)))
finally:
task_queue.task_done()
# Feed the queue
for url in URL_LIST:
task_queue.put(url)
# Start workers
start = time.perf_counter()
workers = [threading.Thread(target=worker, args=(i,), daemon=True)
for i in range(NUM_WORKERS)]
for w in workers: w.start()
task_queue.join() # wait for all URLs to be processed
for _ in workers: # shut down the workers
task_queue.put(None)
for w in workers: w.join()
elapsed = time.perf_counter() - start
results = [] # drain the results
while not result_queue.empty():
results.append(result_queue.get())
print(f"\nFetched {len(results)} URLs in {elapsed:.2f}s using {NUM_WORKERS} threads")
print(f"Sequential would take ~{len(URL_LIST)}s")
50 one-second "downloads" finished in 5 seconds: 10 workers × 5 rounds. Each worker always calls task_done() in finally, so even a failed download is counted and task_queue.join() cannot hang. Notice also that some output lines are glued together: when many threads call print() at once, the text and the newline can interleave. That is why the real results travel through result_queue, not through printed text.
50 एक-second वाले "downloads" 5 seconds में पूरे: 10 workers × 5 rounds। हर worker हमेशा finally में task_done() बुलाता है, इसलिए fail हुआ download भी गिना जाता है और task_queue.join() अटक नहीं सकता। यह भी देखिए कि कुछ output lines आपस में चिपक गई हैं: जब कई threads एक साथ print() बुलाते हैं, तो text और newline आपस में मिल सकते हैं। इसीलिए असली नतीजे printed text से नहीं, result_queue से भेजे जाते हैं।
50 次各 1 秒的"下载"在 5 秒内完成:10 个工作线程 × 5 轮。每个工作线程都在 finally 中调用 task_done(),所以即使下载失败也会被计数,task_queue.join() 不会卡住。另外注意,有些输出行粘在了一起:当许多线程同时调用 print() 时,文字和换行符可能交错。这就是真正的结果要通过 result_queue 传递、而不是靠打印文本的原因。
PriorityQueue — When Some Tasks Matter More 🏆PriorityQueue — जब कुछ Tasks ज़्यादा ज़रूरी हों 🏆PriorityQueue — 当有些任务更重要时 🏆
A support-ticket system where urgent tickets jump the line. The priority number goes first; the lowest number comes out first.
एक support-ticket system जहाँ ज़रूरी tickets कतार तोड़कर आगे जाते हैं। Priority number पहले आता है; सबसे छोटा number पहले निकलता है।
一个支持工单系统,紧急工单可以插队。优先级数字放在最前面;数字最小 的先出。
Put items in any order; get() always returns the smallest priority next.
Items किसी भी क्रम में डालिए; get() हमेशा अगला सबसे छोटा priority लौटाता है।
以任意顺序放入;get() 总是返回当前优先级最小的元素。
import threading
import queue
import time
from dataclasses import dataclass, field
from typing import Any
# Wrap payload so equal-priority items don't try to compare data
@dataclass(order=True)
class Ticket:
priority: int
data: Any = field(compare=False)
pq = queue.PriorityQueue()
# Producers add tickets with varied priority (1=urgent, 5=low)
pq.put(Ticket(3, "Fix typo on homepage"))
pq.put(Ticket(1, "PROD DOWN - payment gateway failing!"))
pq.put(Ticket(5, "Update the copyright year"))
pq.put(Ticket(2, "Enterprise customer login broken"))
pq.put(Ticket(1, "Data breach alert!"))
def handler():
while not pq.empty():
ticket = pq.get()
print(f"[P{ticket.priority}] {ticket.data}")
pq.task_done()
time.sleep(0.2)
threading.Thread(target=handler).start()
The two P1 tickets came out in put() order in this run — but that is luck. With field(compare=False) the two tickets are simply "equal", and a heap does not keep equal items in arrival order. The PriorityQueue Lab below shows a real case where a later ticket comes out first. The original's warning also said "unhashable/unorderable" data causes the error: only orderability matters — hashing is never used. If you want first-come-first-served among equal priorities, add a counter as the tie-breaker: (priority, counter, data).
इस run में दोनों P1 tickets put() क्रम में निकले — पर वह संयोग है। field(compare=False) के साथ दोनों tickets बस "बराबर" हैं, और heap बराबर items को आने के क्रम में नहीं रखता। नीचे का PriorityQueue Lab एक असली case दिखाता है जहाँ बाद वाला ticket पहले निकलता है। मूल की warning में यह भी कहा गया कि "unhashable/unorderable" data से error आता है: सिर्फ़ तुलना हो पाना मायने रखता है — hashing कभी इस्तेमाल नहीं होती। बराबर priorities में पहले-आओ-पहले-पाओ चाहिए तो tie-breaker के रूप में counter जोड़िए: (priority, counter, data)।
这次运行中两个 P1 工单恰好按 put() 的顺序出来了 — 但那只是运气。使用 field(compare=False) 时,两个工单只是"相等"而已,堆不会保持相等元素的到达顺序。下面的 PriorityQueue 实验室展示了一个后放入的工单先出来的真实例子。原文的警告还说"不可哈希/不可排序"的数据会导致错误:其实只有 能否比较大小 才重要 — 哈希从来不会被用到。如果你希望相同优先级先到先得,就加一个计数器作为平局决胜项:(priority, counter, data)。
Tie-breaking — the error and the fixTie-breaking — error और fix平局决胜 — 错误与修复
import queue
pq = queue.PriorityQueue()
pq.put((1, {"task": "backup"}))
pq.put((1, {"task": "deploy"})) # same priority -> compares the dicts!
import queue, itertools
counter = itertools.count() # 0, 1, 2, ... never repeats
pq = queue.PriorityQueue()
for prio, data in [(3, "Fix typo on homepage"), (1, "PROD DOWN - payment gateway failing!"),
(5, "Update the copyright year"), (2, "Enterprise customer login broken"),
(1, "Data breach alert!"), (1, {"task": "a dict is fine now"})]:
pq.put((prio, next(counter), data)) # (priority, tie-breaker, data)
while not pq.empty():
prio, n, data = pq.get()
print(f"[P{prio}] #{n} {data}")
The counter is unique, so Python never needs to compare the data — even a dict works — and equal priorities come out in the order they were put (#1, #4, #5).
Counter अद्वितीय है, इसलिए Python को कभी data की तुलना नहीं करनी पड़ती — dict भी चलता है — और बराबर priorities उसी क्रम में निकलती हैं जिसमें डाली गईं (#1, #4, #5)।
计数器是唯一的,所以 Python 永远不需要比较数据 — 即使是字典也没问题 — 相同优先级的元素按放入顺序取出(#1、#4、#5)。
🏆 Simulator: PriorityQueue Lab🏆 Simulator: PriorityQueue Lab🏆 模拟器:PriorityQueue 实验室
Add tickets with priority 1, 2 or 3 and call get(). The tree shows the real heap inside the queue. Compare the dataclass style with the counter style, or press "show unfair order". Checked against 300 real runs of queue.PriorityQueue.
Priority 1, 2 या 3 के tickets डालिए और get() बुलाइए। Tree queue के अंदर का असली heap दिखाता है। Dataclass तरीक़े की counter तरीक़े से तुलना कीजिए, या "अनुचित क्रम दिखाएँ" दबाइए। queue.PriorityQueue के 300 असली runs से जाँचा गया।
添加优先级为 1、2 或 3 的工单并调用 get()。树形图显示队列内部真实的堆。比较 dataclass 写法和计数器写法,或者点击"演示不公平顺序"。已与 queue.PriorityQueue 的 300 次真实运行核对。
The Modern Way — ThreadPoolExecutor 🏊आधुनिक तरीक़ा — ThreadPoolExecutor 🏊现代做法 — ThreadPoolExecutor 🏊
concurrent.futures.ThreadPoolExecutor is a thread pool with a queue inside and a clean API. It manages the queue, the worker threads and the results for you. For most new batch jobs, this is what to reach for.
concurrent.futures.ThreadPoolExecutor अंदर queue वाला एक thread pool है, साफ़ API के साथ। यह queue, worker threads और नतीजे आपके लिए संभालता है। ज़्यादातर नए batch jobs के लिए यही लीजिए।
concurrent.futures.ThreadPoolExecutor 是一个内含队列、接口简洁的线程池。它替你管理队列、工作线程和结果。对大多数新的批处理任务,首选它。
Under the hood it is a queue plus a pool of worker threads — you just do not wire it yourself.
अंदर से यह एक queue और worker threads का pool है — बस इसे आपको ख़ुद जोड़ना नहीं पड़ता।
它的内部就是一个队列加一组工作线程 — 只是不用你自己搭建。
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def fake_get(url):
"""Stands in for requests.get(url, timeout=10)."""
time.sleep(1) # sleep releases the GIL, just like socket I/O
return 200, b"x" * 308
urls = [f"https://httpbin.org/delay/1?id={i}" for i in range(20)]
def fetch(url):
code, content = fake_get(url)
return url, code, len(content)
start = time.perf_counter()
# The executor manages an internal queue + worker threads
with ThreadPoolExecutor(max_workers=10) as pool:
# submit() returns a Future - puts a task on the queue
futures = {pool.submit(fetch, u): u for u in urls}
# as_completed yields futures as they finish
for future in as_completed(futures):
url, code, size = future.result()
print(f"{code} {size:6d}B {url}")
print(f"\nDone in {time.perf_counter() - start:.2f}s")
Use ThreadPoolExecutor for one-shot batch jobs: submit N tasks, collect N results, done. Use Queue directly for long-running streams, priority handling, or more complex producer-consumer shapes (for example, a crawler whose workers add newly found URLs back to the queue).
एक बार के batch jobs के लिए ThreadPoolExecutor: N tasks submit कीजिए, N नतीजे लीजिए, बस। लंबे चलने वाले streams, priority handling, या ज़्यादा जटिल producer-consumer ढाँचों के लिए सीधे Queue लीजिए (जैसे एक crawler जिसके workers नए मिले URLs वापस queue में डालते हैं)।
一次性的批处理任务用 ThreadPoolExecutor:提交 N 个任务,收集 N 个结果,完事。长期运行的数据流、优先级处理或更复杂的生产者-消费者结构则直接用 Queue(例如一个爬虫,其工作线程会把新发现的 URL 放回队列)。
Pipeline Pattern — Chained Queues 🔗Pipeline Pattern — जुड़ी हुई Queues 🔗流水线模式 — 串联的队列 🔗
Real systems often have stages: download → parse → save. Put a queue between stages, and give each stage its own group of threads.
असली systems में अक्सर stages होते हैं: download → parse → save। Stages के बीच एक queue रखिए, और हर stage को threads का अपना समूह दीजिए।
真实系统常常分成几个阶段:下载 → 解析 → 保存。在阶段之间放一个队列,并给每个阶段配一组自己的线程。
Data flows left to right through queues. Slow stages get more workers.
Data queues से होकर बाएँ से दाएँ बहता है। धीमे stages को ज़्यादा workers मिलते हैं।
数据从左到右流经各个队列。较慢的阶段分配更多工作线程。
import threading
import queue
import time
download_q = queue.Queue()
parse_q = queue.Queue()
save_q = queue.Queue()
def downloader():
while True:
url = download_q.get()
if url is None: break
time.sleep(0.3) # simulated network
html = f"<html>from {url}</html>"
parse_q.put((url, html))
download_q.task_done()
def parser():
while True:
item = parse_q.get()
if item is None: break
url, html = item
time.sleep(0.1) # simulated parsing
record = {"url": url, "len": len(html)}
save_q.put(record)
parse_q.task_done()
def saver():
while True:
rec = save_q.get()
if rec is None: break
print(f"SAVED {rec}")
save_q.task_done()
# Start each stage as a thread pool
threads = []
for _ in range(4): threads.append(threading.Thread(target=downloader))
for _ in range(2): threads.append(threading.Thread(target=parser))
for _ in range(1): threads.append(threading.Thread(target=saver))
for t in threads: t.start()
start = time.perf_counter()
for i in range(20): # feed URLs
download_q.put(f"https://example.com/page{i}")
# Wait for each stage to fully drain, then send poison pills
download_q.join()
for _ in range(4): download_q.put(None)
parse_q.join()
for _ in range(2): parse_q.put(None)
save_q.join()
save_q.put(None)
for t in threads: t.join()
print(f"Pipeline complete in {time.perf_counter() - start:.2f}s.")
The original said a single saver thread "preserves order". Look at the real output: page3 was saved before page2, and page11 before page9. Four downloaders finish at slightly different times, so items reach save_q out of order. One saver only guarantees that saves happen one at a time (no two writes collide) — it does not restore the original order. If order matters, carry the index with each item and sort, or use a single thread per stage.
मूल में कहा गया कि एक saver thread "क्रम बनाए रखता है"। असली output देखिए: page3, page2 से पहले save हुआ, और page11, page9 से पहले। चार downloaders थोड़े अलग समय पर ख़त्म होते हैं, इसलिए items save_q में बिना क्रम के पहुँचते हैं। एक saver सिर्फ़ यह पक्का करता है कि saves एक-एक करके हों (दो writes टकराएँ नहीं) — यह मूल क्रम वापस नहीं लाता। क्रम ज़रूरी हो तो हर item के साथ उसका index रखिए और sort कीजिए, या हर stage में एक ही thread रखिए।
原文说单个保存线程"能保持顺序"。看看真实输出:page3 在 page2 之前保存,page11 在 page9 之前保存。四个下载线程完成的时间略有不同,所以元素进入 save_q 时已经乱序了。一个保存线程只能保证保存 一次一个(不会有两次写入冲突)— 它不会恢复原来的顺序。如果顺序重要,就让每个元素带上自己的序号再排序,或者每个阶段只用一个线程。
🔗 Simulator: Pipeline Lab (real runs)🔗 Simulator: Pipeline Lab (असली runs)🔗 模拟器:流水线实验室(真实运行)
12 pages flow through the same pipeline (download 0.3 s, parse 0.1 s). Choose how many downloaders and parsers to start. Each bar is one page in one worker, from a real run.
12 pages उसी pipeline से गुज़रते हैं (download 0.3 s, parse 0.1 s)। चुनिए कितने downloaders और parsers शुरू हों। हर bar एक worker में एक page है, असली run से।
12 个页面流经同样的流水线(下载 0.3 秒,解析 0.1 秒)。选择启动多少个下载线程和解析线程。每个条形表示某个工作线程处理的一个页面,来自真实运行。
The original code used 4 downloaders, 2 parsers and 1 saver. The slow I/O stage (downloading) gets the most workers. The saver is a single thread so writes to a file or database never collide. Match the worker count to each stage's bottleneck — the Pipeline Lab shows what happens when you do not.
मूल code में 4 downloaders, 2 parsers और 1 saver थे। धीमे I/O stage (downloading) को सबसे ज़्यादा workers मिलते हैं। Saver एक ही thread है ताकि file या database में writes कभी न टकराएँ। Worker count हर stage की रुकावट के हिसाब से रखिए — Pipeline Lab दिखाता है कि ऐसा न करने पर क्या होता है।
原代码使用 4 个下载线程、2 个解析线程和 1 个保存线程。最慢的 I/O 阶段(下载)分配最多的工作线程。保存只用一个线程,这样写入文件或数据库永远不会冲突。让每个阶段的线程数与其瓶颈相匹配 — 流水线实验室展示了不这样做的后果。
Getting Around the GIL — Your Options 🚀GIL से बचने के रास्ते — आपके विकल्प 🚀绕过 GIL — 你的选项 🚀
multiprocessing.Pool or ProcessPoolExecutor for CPU-bound work.multiprocessing.Pool या ProcessPoolExecutor लीजिए।multiprocessing.Pool 或 ProcessPoolExecutor。async/await code.async/await code चाहिए।async/await 代码。np.dot may even use several cores, depending on the BLAS library NumPy was built with.np.dot जैसे बड़े matrix products कई cores भी इस्तेमाल कर सकते हैं, यह NumPy के BLAS library पर निर्भर है।np.dot 这样的大矩阵乘法甚至可能用到多个核心,取决于 NumPy 所链接的 BLAS 库。concurrent.interpreters module to use them. A middle ground between threads and processes.concurrent.interpreters module जोड़ा। Threads और processes के बीच का रास्ता।concurrent.interpreters 模块来使用它们。介于线程和进程之间的折中方案。The original called free-threaded Python "experimental, not the default in 2026" and listed subinterpreters as 3.12-only. Since Python 3.14, free-threading is officially supported (still opt-in), and subinterpreters are usable from pure Python through concurrent.interpreters. We also softened "np.dot uses all cores" — that depends on the BLAS library behind NumPy.
मूल ने free-threaded Python को "experimental, 2026 में default नहीं" कहा और subinterpreters को सिर्फ़ 3.12 वाला बताया। Python 3.14 से free-threading आधिकारिक रूप से supported है (अब भी opt-in), और subinterpreters concurrent.interpreters से pure Python में इस्तेमाल हो सकते हैं। हमने "np.dot सभी cores इस्तेमाल करता है" को भी नरम किया — यह NumPy के पीछे की BLAS library पर निर्भर है।
原文称自由线程 Python"是实验性的,2026 年也不是默认",并把子解释器列为仅限 3.12。从 Python 3.14 起,自由线程已获得官方支持(仍需主动选择),子解释器也可以通过 concurrent.interpreters 在纯 Python 中使用。我们还把"np.dot 会用满所有核心"改得更谨慎 — 这取决于 NumPy 背后的 BLAS 库。
Common Pitfalls and Fixes 🩺आम ग़लतियाँ और उनके Fix 🩺常见陷阱与修复 🩺
task_done() → q.join() waits for ever. Pair every get() with a task_done(), even on errors — use try/finally.task_done() भूलना → q.join() हमेशा इंतज़ार करता है। हर get() के साथ task_done() रखिए, error पर भी — try/finally लीजिए।task_done() → q.join() 永远等待。每个 get() 都要配一个 task_done(),出错时也一样 — 用 try/finally。get() for ever and the program never ends. Send one poison pill (None) per consumer.get() पर हमेशा इंतज़ार करते हैं और program कभी ख़त्म नहीं होता। हर consumer के लिए एक poison pill (None) भेजिए।get() 上等待,程序永远不结束。给每个消费者发送一个毒丸(None)。Queue(maxsize=1000) so producers wait (backpressure).Queue(maxsize=1000) लीजिए ताकि producers रुकें (backpressure)।Queue(maxsize=1000),让生产者等待(背压)。join() them; use daemon=True only for workers that hold nothing important.join() कीजिए; daemon=True सिर्फ़ उन workers के लिए जो कुछ ज़रूरी नहीं रखते।join() 它们;daemon=True 只用于不保存重要数据的工作线程。task_done(), and main does not know. Catch exceptions in the worker and send them to an error queue.task_done() नहीं हुआ, और main को पता नहीं। Worker में exceptions पकड़िए और उन्हें error queue में भेजिए।task_done(),主线程也不知道。在工作线程中捕获异常,把它们送进错误队列。t.join(timeout=0.5) in a loop.t.join(timeout=0.5) से इंतज़ार कीजिए।t.join(timeout=0.5) 等待。The original said an exception in a worker "just kills that thread with no message". In fact Python prints the full traceback to stderr ("Exception in thread …") — you can see it in the Bug Doctor below. The real problem is that nothing else reacts: the thread is gone, q.join() waits for ever, and the main thread never learns about the error.
मूल में कहा गया कि worker में exception "बिना किसी message के उस thread को मार देता है"। असल में Python stderr पर पूरा traceback print करता है ("Exception in thread …") — नीचे Bug Doctor में देख सकते हैं। असली समस्या यह है कि और कुछ प्रतिक्रिया नहीं देता: thread चला गया, q.join() हमेशा इंतज़ार करता है, और main thread को error का कभी पता नहीं चलता।
原文说工作线程中的异常"只会悄悄杀死那个线程,没有任何消息"。实际上 Python 会把完整的回溯打印到 stderr("Exception in thread …")— 你可以在下面的 Bug 医生中看到。真正的问题是没有别的东西作出反应:线程没了,q.join() 永远等待,主线程也永远不知道出了错。
Every production worker should look like this: take an item, check for the poison pill, wrap the real work in try/except, and always call task_done() in finally. This removes most threading bugs.
हर production worker ऐसा दिखना चाहिए: item लीजिए, poison pill जाँचिए, असली काम को try/except में रखिए, और finally में हमेशा task_done() बुलाइए। इससे ज़्यादातर threading bugs हट जाते हैं।
每个生产环境的工作线程都应该是这样:取一个元素,检查毒丸,把真正的工作包在 try/except 中,并且总在 finally 中调用 task_done()。这能消除大多数线程 bug。
import queue, threading
def process(item): # your real work
if item == "bad":
raise ValueError("cannot process 'bad'")
print("processed", item)
def safe_worker(work_q, error_q):
while True:
item = work_q.get()
try:
if item is None: # poison pill
break
process(item)
except Exception as e:
error_q.put((item, repr(e))) # never let exceptions vanish
finally:
work_q.task_done() # ALWAYS signal completion
work_q, error_q = queue.Queue(), queue.Queue()
t = threading.Thread(target=safe_worker, args=(work_q, error_q))
t.start()
for item in ["a", "bad", "b", None]:
work_q.put(item)
work_q.join() # returns - every get() had a task_done()
t.join()
print("errors:", list(error_q.queue))
🩺 Simulator: Bug Doctor🩺 Simulator: Bug Doctor🩺 模拟器:Bug 医生
Six real mistakes from this tutorial. Buggy code on the left, fixed code on the right, changed lines marked. Every output is from a real run (programs that would hang were checked with a 1-second timeout).
इस tutorial की छह असली ग़लतियाँ। बाईं ओर buggy code, दाईं ओर ठीक किया हुआ, बदली lines चिह्नित। हर output असली run से है (जो programs अटक जाते उन्हें 1-second timeout से जाँचा गया)।
本教程中的六个真实错误。左边是有 bug 的代码,右边是修正后的代码,改动的行已标出。每个输出都来自真实运行(会挂起的程序用 1 秒超时进行检查)。
Threads vs Processes vs asyncio — Decision Table ⚖️Threads बनाम Processes बनाम asyncio — फ़ैसले की Table ⚖️线程 vs 进程 vs asyncio — 决策表 ⚖️
| Propertyगुण特性 | threading + Queue | multiprocessing | asyncio |
|---|---|---|---|
| Best forकिसके लिए最适合 | I/O-bound (up to ~100 workers)I/O-bound (~100 workers तक)I/O 密集(最多约 100 个工作者) | CPU-boundCPU-boundCPU 密集 | I/O-bound at huge scale (1000+)बहुत बड़े पैमाने पर I/O (1000+)超大规模 I/O(1000+) |
| Gets around the GIL?GIL से बचता है?能绕过 GIL? | No — but it is released during I/Oनहीं — पर I/O के दौरान छूटता है否 — 但 I/O 期间会释放 | Yes — each process has its ownहाँ — हर process का अपना是 — 每个进程各有一个 | No — one threadनहीं — एक thread否 — 单线程 |
| Sharing dataData बाँटना共享数据 | Easy — same memory (needs locks/queues)आसान — वही memory (locks/queues चाहिए)容易 — 同一内存(需要锁/队列) | Costly — data is pickled and copiedमहँगा — data pickle होकर copy होता है代价高 — 数据要序列化并复制 | Easy — same event loopआसान — वही event loop容易 — 同一个事件循环 |
| Cost per unitहर unit की क़ीमत每个单元的开销 | Stack reserved per thread (8 MB virtual on Linux; real use is far smaller)हर thread का reserved stack (Linux पर 8 MB virtual; असली इस्तेमाल बहुत कम)每个线程预留栈(Linux 上 8 MB 虚拟内存;实际占用小得多) | A whole interpreter — often tens of MBपूरा interpreter — अक्सर दसियों MB一个完整的解释器 — 常为数十 MB | A few KB per coroutineहर coroutine के कुछ KB每个协程几 KB |
| Start-up costशुरू करने की क़ीमत启动开销 | Fast (~ms)तेज़ (~ms)快(约毫秒级) | Slower (fork is fast; spawn can take ~100 ms)धीमा (fork तेज़; spawn ~100 ms ले सकता है)较慢(fork 快;spawn 可能需约 100 毫秒) | Instantतुरंत瞬间 |
| Existing sync code works?मौजूदा sync code चलेगा?现有同步代码能用吗? | Yes — unchangedहाँ — बिना बदले能 — 无需修改 | Yes — mostly unchangedहाँ — लगभग बिना बदले能 — 基本无需修改 | No — needs an async rewriteनहीं — async में दोबारा लिखना होगा不能 — 需要改写成 async |
| Debugging difficultyDebugging कठिनाई调试难度 | Medium (race conditions)मध्यम (race conditions)中等(竞态条件) | Medium (inter-process issues)मध्यम (inter-process समस्याएँ)中等(进程间通信问题) | Higher (cooperative scheduling)ज़्यादा (cooperative scheduling)较高(协作式调度) |
The original table said each thread costs "~8 MB". On Linux that is the default stack reservation (virtual address space). The operating system only backs the pages a thread actually touches, so a typical Python thread uses far less real memory. Threads are cheap; hundreds of them are fine. The real limits are usually remote servers and connection pools, not RAM.
मूल table में हर thread की क़ीमत "~8 MB" बताई गई। Linux पर यह default stack का reservation (virtual address space) है। Operating system सिर्फ़ उन pages को असली memory देता है जिन्हें thread सच में छूता है, इसलिए आम Python thread बहुत कम असली memory लेता है। Threads सस्ते हैं; सैकड़ों भी ठीक हैं। असली सीमा आम तौर पर remote servers और connection pools होते हैं, RAM नहीं।
原表格说每个线程要"约 8 MB"。在 Linux 上,这是默认的栈 预留(虚拟地址空间)。操作系统只为线程真正用到的页面分配实际内存,所以一个普通的 Python 线程实际占用的内存要少得多。线程很便宜,几百个也没问题。真正的限制通常是远端服务器和连接池,而不是内存。
Golden Rules and Quiz 🏆सुनहरे नियम और Quiz 🏆黄金法则与小测验 🏆
multiprocessing or a C extension like NumPy.multiprocessing या NumPy जैसा C extension लीजिए।multiprocessing 或 NumPy 这样的 C 扩展。queue.Queue, not through shared mutable objects. The queue's internal locks remove most race conditions.queue.Queue से भेजिए, साझा बदलने वाले objects से नहीं। Queue के अंदर के locks ज़्यादातर race conditions हटा देते हैं।queue.Queue 传递数据,而不是通过共享的可变对象。队列内部的锁能消除绝大多数竞态条件。q.get() with a q.task_done() in a finally block. Otherwise q.join() waits for ever when a worker hits an error.q.get() के साथ finally block में q.task_done() रखिए। वरना worker को error मिलने पर q.join() हमेशा इंतज़ार करता है।q.get() 都要在 finally 块中配一个 q.task_done()。否则工作线程出错时 q.join() 会永远等待。maxsize on queues in long-running pipelines. An unbounded queue fills memory when producers outrun consumers.maxsize set कीजिए। Producers के consumers से आगे निकलने पर असीमित queue memory भर देती है।maxsize。生产者比消费者快时,无界队列会占满内存。None per worker after the real work is done. Do not rely on daemon=True alone to stop workers that hold state.None। State रखने वाले workers को रोकने के लिए सिर्फ़ daemon=True पर भरोसा मत कीजिए।None。不要只依靠 daemon=True 来停止持有状态的工作线程。t.join(timeout=0.5) in a loop.t.join(timeout=0.5) से इंतज़ार कीजिए।t.join(timeout=0.5) 等待。concurrent.futures.ThreadPoolExecutor over hand-made thread + queue code. It manages the queue, the pool and the results in a few lines.concurrent.futures.ThreadPoolExecutor लीजिए। यह कुछ lines में queue, pool और नतीजे संभालता है।concurrent.futures.ThreadPoolExecutor,而不是手写的线程 + 队列代码。它用几行代码就管理好了队列、线程池和结果。ThreadPoolExecutor's default is min(32, os.cpu_count() + 4) workers. For I/O you can often use more, but server rate limits and connection pools usually cap what helps.ThreadPoolExecutor का default min(32, os.cpu_count() + 4) workers है। I/O के लिए अक्सर ज़्यादा ले सकते हैं, पर server rate limits और connection pools आम तौर पर फ़ायदे की सीमा तय करते हैं।ThreadPoolExecutor 的默认值是 min(32, os.cpu_count() + 4) 个工作线程。I/O 工作常常可以用更多,但服务器的速率限制和连接池通常决定了有效的上限。The original rule of thumb was min(32, os.cpu_count() * 4). Python's own default for ThreadPoolExecutor (since 3.8) is min(32, os.cpu_count() + 4) — plus 4, not times 4.
मूल का अंदाज़ा min(32, os.cpu_count() * 4) था। ThreadPoolExecutor के लिए Python का अपना default (3.8 से) min(32, os.cpu_count() + 4) है — 4 जोड़ना, 4 से गुणा नहीं।
原文的经验公式是 min(32, os.cpu_count() * 4)。Python 自己为 ThreadPoolExecutor 设定的默认值(自 3.8 起)是 min(32, os.cpu_count() + 4) — 是加 4,不是乘 4。
Quick Quiz — 8 Questionsछोटा Quiz — 8 सवाल小测验 — 8 道题
In standard CPython the GIL lets only one thread run Python code at a time, handing over about every 5 ms. So threads do not speed up CPU-bound work — use processes — but they are excellent for I/O, because the GIL is released while threads wait. Connect threads with queue.Queue: pair get() with task_done(), bound the queue with maxsize, stop workers with poison pills, and reach for ThreadPoolExecutor for simple batch jobs.
Standard CPython में GIL एक समय पर सिर्फ़ एक thread को Python code चलाने देता है, लगभग हर 5 ms पर सौंपते हुए। इसलिए threads CPU-bound काम तेज़ नहीं करते — processes लीजिए — पर I/O के लिए बढ़िया हैं, क्योंकि threads के इंतज़ार के दौरान GIL छूटता है। Threads को queue.Queue से जोड़िए: get() के साथ task_done(), queue को maxsize से सीमित कीजिए, workers को poison pills से रोकिए, और सरल batch jobs के लिए ThreadPoolExecutor लीजिए।
在标准 CPython 中,GIL 同一时刻只让一个线程运行 Python 代码,大约每 5 毫秒交接一次。所以线程无法加速 CPU 密集型工作 — 请用进程 — 但它们非常适合 I/O,因为线程等待时会释放 GIL。用 queue.Queue 连接线程:get() 与 task_done() 配对,用 maxsize 限制队列大小,用毒丸停止工作线程,简单的批处理任务就用 ThreadPoolExecutor。