Python Advance 📂 Threads · 3 of 4 423 min read

Python GIL & Threading with Queue

Master Python's Global Interpreter Lock and thread-safe queues in one deep-dive. Learn why the GIL exists, when it hurts (CPU-bound) and when it doesn't (I/O-bound), and how to build production-grade producer-consumer systems with queue.Queue, PriorityQueue, and ThreadPoolExecutor. Includes benchmarks, real-world web scraper, chained pipelines, poison-pill shutdown, and a golden-rules cheat sheet.

🌐
SectionSection第 01 节

The Story That Explains the GIL 🎤GIL समझाने वाली कहानी 🎤一个讲清 GIL 的故事 🎤

The Single Microphone at a Debateबहस में एक ही Microphone辩论会上唯一的麦克风
Imagine a debate hall with 8 speakers on stage, but only one microphone. Only the person holding the microphone can speak. Everyone else must wait, no matter how eager they are. The mic is passed around, but at any instant exactly one voice is heard.

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).
मान लीजिए एक बहस हॉल में मंच पर 8 वक्ता हैं, पर microphone सिर्फ़ एक है। सिर्फ़ microphone पकड़ने वाला ही बोल सकता है। बाक़ी सबको इंतज़ार करना होगा, चाहे वे कितने भी उत्सुक हों। Mic घूमता रहता है, पर किसी भी पल ठीक एक आवाज़ सुनाई देती है।

Mic इतनी तेज़ी से घूमता है कि दर्शकों को लगता है सभी 8 एक साथ बोल रहे हैं। असल में हमेशा एक ही बोल रहा होता है। और वक्ता जोड़ने से कुछ नहीं होता — microphone ही रुकावट है।

वही एक microphone Python का Global Interpreter Lock (GIL) है। 8 वक्ता आपके threads हैं। आपके पास कितने भी CPU cores हों, एक समय पर सिर्फ़ एक thread Python bytecode चलाता है (standard CPython build में)।
想象一个辩论大厅,台上有 8 位发言者,但只有 一支麦克风。只有拿着麦克风的人才能发言,其他人无论多着急都只能等待。麦克风在大家之间传递,但任何时刻都只有一个声音。

麦克风传得太快,观众以为 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 密集型的多线程程序。

🎬
The Core Insightमूल बात核心要点

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 密集型 工作 — 那种情况请使用进程。

▶️
Six Simulators and a Quizछह Simulators और एक Quiz六个模拟器和一个小测验

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 运行。


SectionSection第 02 节

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 字节码。一个线程持有它时,其他线程只能等待。

Figure 1 — The GIL Bottleneck: Many Threads, One Lockचित्र 1 — GIL रुकावट: कई Threads, एक Lock图 1 — GIL 瓶颈:多个线程,一把锁
The GIL bottleneckThread 1Thread 2Thread 3Thread 4🔒 GILone holder at a timeएक समय में एक一次只有一个持有者PYTHON INTERPRETERPYTHON INTERPRETERPYTHON 解释器runs bytecode for Thread 1Thread 1 का bytecode चलाता है为线程 1 执行字节码C1C2C3C4CPU cores: only one runs Python codeCPU cores: सिर्फ़ एक Python code चलाता हैCPU 核心:只有一个在运行 Python 代码Thread 1 holds the GIL — the others waitThread 1 के पास GIL — बाक़ी इंतज़ार में线程 1 持有 GIL — 其他线程等待

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 字节码。

🔑 The life of a thread under the GILGIL के नीचे एक thread का जीवन线程在 GIL 下的一生
Step 1
The thread asks for the GIL. If another thread holds it, this thread waits.
Thread GIL माँगता है। अगर कोई दूसरा thread उसे रखे है, तो यह इंतज़ार करता है।
线程请求 GIL。如果另一个线程持有它,这个线程就等待。
Step 2
With the GIL, the thread runs Python bytecode until it starts a blocking call (I/O, 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.
GIL मिलने पर thread Python bytecode चलाता है, जब तक वह कोई blocking call (I/O, sleep) शुरू न करे — या जब तक कोई दूसरा thread switch interval (Python 3.2 से default 5 ms) तक इंतज़ार करके उसे छोड़ने को न कहे।
拿到 GIL 后,线程执行 Python 字节码,直到它开始一个阻塞调用(I/O、sleep)— 或者另一个线程已经等待了一个 切换间隔(自 Python 3.2 起默认 5 毫秒)并要求它让出。
Step 3
The thread releases the GIL and a waiting thread is woken up.
Thread GIL छोड़ देता है और एक इंतज़ार करता thread जगाया जाता है।
线程释放 GIL,一个等待中的线程被唤醒。
Step 4
That thread takes the GIL, runs its slice, and the cycle repeats.
वह thread GIL लेता है, अपना हिस्सा चलाता है, और चक्र दोहराता है।
那个线程拿到 GIL,运行自己的时间片,循环往复。
Result
Threads take turns. Their Python code does not run in true parallel on several cores.
Threads बारी-बारी से चलते हैं। उनका Python code कई cores पर सच में parallel नहीं चलता।
线程轮流运行。它们的 Python 代码不会在多个核心上真正并行。

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())
OUTPUT
GIL enabled? True switch interval: 0.005 s threads running: 1
⚠️
Common Misconceptionआम ग़लतफ़हमी常见误解

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 等库内部的文件和套接字调用),其他线程可以真正向前推进。


SectionSection第 03 节

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 用 引用计数 来释放对象。没有全局锁的话,两个线程可能同时修改一个对象的引用计数,破坏内存并让解释器崩溃。

🧠
Reason 1 — Memory Safetyकारण 1 — Memory सुरक्षा原因 1 — 内存安全
reference countingreference counting引用计数
Every Python object has a refcount. When it reaches zero, the object is freed. One global lock is a simple, fast way to make every refcount change safe, without a lock on every object.
हर Python object का एक refcount होता है। शून्य होने पर object मुक्त होता है। एक global lock हर refcount बदलाव को सुरक्षित बनाने का सरल, तेज़ तरीक़ा है, हर object पर lock के बिना।
每个 Python 对象都有一个引用计数。计数归零时对象被释放。一把全局锁是让每次引用计数修改都安全的简单、快速的方法,无需给每个对象加锁。
⚡
Reason 2 — Single-Thread Speedकारण 2 — Single-Thread गति原因 2 — 单线程速度
one lock is cheapएक lock सस्ता है一把锁开销小
Taking one uncontended lock is cheaper than taking many small locks. Most Python programs are single-threaded, so the GIL keeps them fast.
एक बिना-होड़ वाला lock लेना कई छोटे locks लेने से सस्ता है। ज़्यादातर Python programs single-threaded हैं, इसलिए GIL उन्हें तेज़ रखता है।
获取一把无竞争的锁比获取许多小锁开销更小。大多数 Python 程序是单线程的,所以 GIL 让它们保持快速。
📚
Reason 3 — C Extensionsकारण 3 — C Extensions原因 3 — C 扩展
ecosystem compatibilityecosystem compatibility生态兼容
Thousands of C extensions (NumPy, Pillow, database drivers) were written assuming the GIL protects them. Removing it means updating that whole ecosystem — the work behind PEP 703.
हज़ारों C extensions (NumPy, Pillow, database drivers) यह मानकर लिखे गए कि GIL उनकी रक्षा करता है। इसे हटाने का मतलब पूरे ecosystem को update करना — PEP 703 के पीछे यही काम है।
成千上万的 C 扩展(NumPy、Pillow、数据库驱动)在编写时都假定 GIL 会保护它们。去掉它意味着更新整个生态 — 这正是 PEP 703 背后的工作。
🔧
Fixed: The Free-Threaded Build Is No Longer Just "Experimental"सुधार: Free-Threaded Build अब सिर्फ़ "Experimental" नहीं已修正:自由线程版本已不再只是"实验性"

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 存在来设计。


SectionSection第 04 节

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 代码。

🛠️ What happens on thread.start()thread.start() पर क्या होता हैthread.start() 时发生了什么
Step 1
Python creates a PyThreadState struct that holds this thread's interpreter state.
Python एक PyThreadState struct बनाता है जो इस thread की interpreter state रखता है।
Python 创建一个 PyThreadState 结构体,保存这个线程的解释器状态。
Step 2
A new native OS thread is started (pthread_create on Unix).
एक नया native OS thread शुरू होता है (Unix पर pthread_create)।
启动一个新的原生操作系统线程(Unix 上是 pthread_create)。
Step 3
The new thread takes the GIL and calls your Python function.
नया thread GIL लेता है और आपका Python function बुलाता है।
新线程获取 GIL 并调用你的 Python 函数。
Step 4
The OS kernel schedules the thread onto a CPU core. Python has no say in which core or when.
OS kernel thread को किसी CPU core पर चलाता है। कौन-सा core और कब, इसमें Python का कोई दख़ल नहीं।
操作系统内核把线程调度到某个 CPU 核心上。用哪个核心、何时运行,Python 无权决定。

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()
OUTPUT
MainThread Python ident=69440 OS thread id=757 Thread-1 (show) Python ident=10080 OS thread id=758 Thread-2 (show) Python ident=17376 OS thread id=759Thread-3 (show) Python ident=24672 OS thread id=760

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;
🔧
Fixed: PyThreadState Is Not "< 100 Bytes", and the Internals Have Changedसुधार: PyThreadState "< 100 Bytes" नहीं है, और Internals बदल चुके हैं已修正:PyThreadState 并不"小于 100 字节",内部实现也已改变

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 的那个。

🔑
Why This Mattersयह क्यों मायने रखता है为什么这很重要

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 执行。


SectionSection第 05 节

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 之间变化很大。

🔴 Python 2.x — tick-based (old)Python 2.x — tick-based (पुराना)Python 2.x — 基于 tick(旧)
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 争夺战"
🟢 Python 3.2+ — time-based (modern)Python 3.2+ — time-based (आधुनिक)Python 3.2+ — 基于时间(现代)
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
Figure 2 — Old (Tick) vs Modern (Time) GIL Switchingचित्र 2 — पुराना (Tick) बनाम आधुनिक (Time) GIL Switching图 2 — 旧(tick)与新(时间)的 GIL 切换
Tick vs time GIL switchingPYTHON 2 — every 100 ticks (uneven time)PYTHON 2 — हर 100 ticks (असमान समय)PYTHON 2 — 每 100 个 tick(时间不均)T1T21 C call = 1 tick!PYTHON 3.2+ — hand-off about every 5 msPYTHON 3.2+ — लगभग हर 5 ms पर hand-offPYTHON 3.2+ — 大约每 5 毫秒交接一次T1T25 ms5 ms5 ms5 ms5 ms5 ms5 ms5 ms5 ms5 ms5 mseven, predictable turns — but still one thread at a timeबराबर, अनुमानित बारी — पर फिर भी एक समय में एक thread均匀、可预测的轮换 — 但仍然一次只有一个线程

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)
OUTPUT
3 RESUME 0 4 LOAD_FAST 0 (n) LOAD_CONST 1 (0) COMPARE_OP 148 (bool(>)) POP_JUMP_IF_FALSE 25 (to L3) 5 L1: LOAD_GLOBAL 1 (print + NULL) LOAD_FAST 0 (n) CALL 1 POP_TOP 6 LOAD_FAST 0 (n) LOAD_CONST 2 (1) BINARY_OP 23 (-=) STORE_FAST 0 (n) 4 LOAD_FAST 0 (n) LOAD_CONST 1 (0) COMPARE_OP 148 (bool(>)) POP_JUMP_IF_FALSE 2 (to L2) JUMP_BACKWARD 24 (to L1) L2: RETURN_CONST 0 (None) L3: RETURN_CONST 0 (None)
🔧
Fixed: The Original Showed Python 2 Bytecodeसुधार: मूल में Python 2 Bytecode दिखाया गया था已修正:原文展示的是 Python 2 字节码

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()。

⚠️
Ticks Were Not Time-Based (Python 2)Ticks समय पर आधारित नहीं थे (Python 2)Tick 不是基于时间的(Python 2)

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 TimelineGIL TimelineGIL 时间线12 real traces12 असली traces12 条真实追踪
Workload:Workload:工作负载:
Switch interval:Switch interval:切换间隔:

SectionSection第 06 节

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 并运行。

Figure 3 — GIL Timeline During I/O (T1 and T2)चित्र 3 — I/O के दौरान GIL Timeline (T1 और T2)图 3 — I/O 期间的 GIL 时间线(T1 与 T2)
GIL timeline during I/OT1T2GILrunI/O wait…runrunI/OrunT1T2T1T2↓ T1 starts I/O: releases the GIL↑ I/O done: T1 takes the GIL back

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 密集程序的原因。

🔄 The four momentsचार पल四个时刻
A
Thread runs Python code (holds the GIL). Arithmetic, list work, function calls. Other threads wait.
Thread Python code चलाता है (GIL रखे है)। गणित, list का काम, function calls। बाक़ी threads इंतज़ार करते हैं।
线程运行 Python 代码(持有 GIL)。算术、列表操作、函数调用。其他线程等待。
B
Thread starts I/O — releases the GIL. Calls like socket.recv(), file.read() or time.sleep() release the GIL just before they block.
Thread I/O शुरू करता है — GIL छोड़ता है। socket.recv(), file.read() या time.sleep() जैसी calls रुकने से ठीक पहले GIL छोड़ देती हैं।
线程开始 I/O — 释放 GIL。socket.recv()、file.read() 或 time.sleep() 这类调用会在阻塞前释放 GIL。
C
Another thread takes the GIL and runs. It may soon start I/O and release it again.
दूसरा thread GIL लेकर चलता है। वह भी जल्दी I/O शुरू करके इसे फिर छोड़ सकता है।
另一个线程拿到 GIL 并运行。它可能很快也开始 I/O 并再次释放。
D
I/O finishes — the thread must get the GIL back before it can handle the result. If another thread is busy with Python code, it waits (Section 11 measures this wait).
I/O ख़त्म — नतीजा संभालने से पहले thread को GIL वापस पाना होगा। अगर कोई दूसरा thread Python code में व्यस्त है, तो यह इंतज़ार करता है (Section 11 यह इंतज़ार मापता है)।
I/O 完成 — 线程必须重新拿到 GIL 才能处理结果。如果另一个线程正忙于 Python 代码,它就得等(第 11 节会测量这个等待)。
🎯
The I/O Sweet SpotI/O का सबसे अच्छा मौक़ाI/O 的最佳场景

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 大部分时间都空着。


SectionSection第 07 节

CPU-Bound vs I/O-Bound — When the GIL Bites 📈CPU-Bound बनाम I/O-Bound — GIL कब काटता है 📈CPU 密集 vs I/O 密集 — GIL 何时"咬人" 📈

Figure 4 — Wall-Clock Time: Threads vs Sequential vs Processesचित्र 4 — असली समय: Threads बनाम Sequential बनाम Processes图 4 — 实际耗时:线程 vs 顺序 vs 进程
Wall-clock time: threads vs sequential vs processesCPU-BOUND (our run, 2 cores)CPU-BOUND (हमारा run, 2 cores)CPU 密集(我们的运行,2 核)I/O-BOUND (10 × 1 s waits)I/O-BOUND (10 × 1 s इंतज़ार)I/O 密集(10 次 1 秒等待)sequential 1.28s2 threads 1.32s2 processes 0.70ssequential 10.00s10 threads 1.00sthreads: no gain · processes: 1.8× fasterthreads: कोई फ़ायदा नहीं · processes: 1.8× तेज़线程:无收益 · 进程:快 1.8 倍threads: 10× fasterthreads: 10× तेज़线程:快 10 倍

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 节的真实数据。同样的线程代码得到相反的结果 — 由工作类型决定。

🔴 CPU-bound (the GIL hurts)CPU-bound (GIL नुक़सान करता है)CPU 密集(GIL 有害)
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
🟢 I/O-bound (the GIL hardly matters)I/O-bound (GIL से फ़र्क़ नहीं)I/O 密集(GIL 影响很小)
WorkloadWorkload工作负载Threads help?Threads मदद करें?线程有帮助吗?
HTTP requests to 100 URLs100 URLs को HTTP requests请求 100 个 URLYes — 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 का बीच वाला मान है।

同样的工作量,在我们的双核机器上用三种方式完成:依次执行、用线程、用进程。每个条形都是三次真实运行的中位数。

Speed LabSpeed Lab速度实验室42 real runs42 असली runs42 次真实运行
Work:काम:工作:
Workers:Workers:工作者数:
🎯
The Golden Ruleसुनहरा नियम黄金法则

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 密集型。


SectionSection第 08 节

The Beazley Experiment — When 2 Threads Were Slower Than 1 🐢Beazley Experiment — जब 2 Threads 1 से धीमे थे 🐢Beazley 实验 — 当 2 个线程比 1 个还慢 🐢

David Beazley's 2009 BombshellDavid Beazley का 2009 का धमाकाDavid Beazley 2009 年的重磅发现
In 2009 David Beazley ran a tiny CPU-bound benchmark on a dual-core MacBook with Python 2.6. Running the work twice, one after the other, took 24.6 s. Running the same work in 2 threads took 45.5 s — about 1.8× slower.

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.
2009 में David Beazley ने एक dual-core MacBook पर Python 2.6 के साथ छोटा-सा CPU-bound benchmark चलाया। काम को दो बार, एक के बाद एक, चलाने में 24.6 s लगे। वही काम 2 threads में चलाने में 45.5 s लगे — लगभग 1.8× धीमा।

फिर उन्होंने एक CPU core बंद किया, और threaded version 38 s पर आ गया। एक core पर यह दो से तेज़ था!

उनकी talk "Inside the Python GIL" सबसे मशहूर Python performance talks में से एक बनी। इसने दिखाया कि पुराना GIL multicore machines पर सिर्फ़ बेकार नहीं था — वह नुक़सान भी कर सकता था।
2009 年,David Beazley 在一台双核 MacBook 上用 Python 2.6 运行了一个很小的 CPU 密集基准测试。把工作依次运行两遍用了 24.6 秒。把同样的工作放到 2 个线程中运行却用了 45.5 秒 — 大约 慢 了 1.8 倍。

然后他关掉了一个 CPU 核心,多线程版本降到了 38 秒。单核居然比双核还快!

他的演讲《Inside the Python GIL》成为最著名的 Python 性能演讲之一。它表明旧的 GIL 在多核机器上不只是没用 — 甚至会帮倒忙。
Figure 5 — The Beazley Numbers (Python 2.6, 2009)चित्र 5 — Beazley के Numbers (Python 2.6, 2009)图 5 — Beazley 的数据(Python 2.6,2009 年)
The Beazley numbersPYTHON 2.6 · 2009 · dual-core Mac (Beazley)PYTHON 2.6 · 2009 · dual-core Mac (Beazley)PYTHON 2.6 · 2009 · 双核 Mac(Beazley)sequential 24.6 s2 threads, 2 cores 45.5 s2 threads, 1 core 38.0 sadding a core made it SLOWER — the old multicore "GIL battle"core जोड़ने से यह धीमा हुआ — पुराना multicore "GIL battle"加一个核心反而更慢 — 旧的多核"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")
OUTPUT
Sequential : 1.08s Threaded : 1.11s
🔧
Fixed: On Modern Python, Threads Are No Longer 1.8× Slowerसुधार: आधुनिक Python पर Threads अब 1.8× धीमे नहीं已修正:在现代 Python 上,线程不再慢 1.8 倍

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 代码 — 但它们也不再让代码大幅变慢。

🔥
Why Was One Core Faster? (Python 2)एक Core तेज़ क्यों था? (Python 2)为什么单核更快?(Python 2)

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 यह लड़ाई और इसकी क़ीमत दिखाते हैं।

单核时只有一个线程能运行,所以没有浪费的信号。双核时操作系统让两个线程同时运行,它们陷入不停的争夺:释放、发信号、唤醒、失败、睡眠 — 每秒数千次系统调用都花在了锁上,而不是你的工作上。接下来两节展示这场争夺及其代价。


SectionSection第 09 节

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 下,它们就开始争抢这唯一的锁。

Figure 6 — Two Cores, One GIL: The Battle (Python 2)चित्र 6 — दो Cores, एक GIL: लड़ाई (Python 2)图 6 — 两个核心,一把 GIL:争夺战(Python 2)
Two cores, one GIL: the battleCORE 1CORE 1核心 1T1 runningCORE 2CORE 2核心 2T2 sleeping🔒 GILreleasesignalre-grab!T2 wakes → tries → FAILS → sleepsT2 wakes → tries → FAILS → sleeps… hundreds of times per second …T1 keeps winning because it is already awake on Core 1T1 जीतता रहता है क्योंकि वह Core 1 पर पहले से जगा हैT1 一直赢,因为它已经在核心 1 上醒着

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 甚至还没来得及醒来。

⚔️ The battle, step by step (old GIL)लड़ाई, क़दम-दर-क़दम (पुराना GIL)争夺过程,一步步来(旧 GIL)
01
T1 (Core 1) releases the GIL at a check and signals a waiting thread.
T1 (Core 1) एक जाँच पर GIL छोड़ता है और इंतज़ार करते thread को signal देता है।
T1(核心 1)在检查点释放 GIL,并通知一个等待中的线程。
02
T2 (Core 2) is woken by the signal.
T2 (Core 2) signal से जागता है।
T2(核心 2)被信号唤醒。
03
T1 is still running on Core 1, so it takes the GIL again first. T2's attempt fails.
T1 अब भी Core 1 पर चल रहा है, इसलिए वह पहले GIL फिर ले लेता है। T2 की कोशिश fail होती है।
T1 仍在核心 1 上运行,所以它先一步再次拿到 GIL。T2 的尝试失败。
04
T2 goes back to sleep — a pointless wake-up. Soon it is signalled again, fails again, sleeps again.
T2 फिर सो जाता है — बेकार का जागना। जल्द ही फिर signal, फिर fail, फिर सोना।
T2 又睡回去 — 白醒一场。很快它又被唤醒,又失败,又睡去。
05
Hundreds of failed attempts per second. Each one costs system calls and context switches — about 9.5 million Mach system calls in Beazley's 2-core trace.
हर second सैकड़ों असफल कोशिशें। हर एक की क़ीमत system calls और context switches — Beazley के 2-core trace में लगभग 95 लाख Mach system calls।
每秒数百次失败的尝试。每次都要付出系统调用和上下文切换的代价 — Beazley 的双核追踪中约有 950 万次 Mach 系统调用。

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
🔧
Fixed: This Battle Is Python 2 History — Python 3.2 Changed the Hand-Offसुधार: यह लड़ाई Python 2 का इतिहास है — Python 3.2 ने Hand-Off बदला已修正:这场争夺是 Python 2 的历史 — Python 3.2 改变了交接方式

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 执行。

🚫
The Root Cause — Two Schedulersअसली वजह — दो Schedulers根本原因 — 两个调度者

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 代码。


SectionSection第 10 节

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)736117Baseline — no threading costBaseline — threading की कोई क़ीमत नहीं基线 — 没有线程开销
2 CPU-bound threads (1 CPU)2 CPU-bound threads (1 CPU)2 个 CPU 密集线程(1 CPU)1,149~3,300,0003.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,000Nearly 3× worse on multicoreMulticore पर लगभग 3× बदतर多核上差了近 3 倍
💸
The Hidden Costछिपी क़ीमत隐藏的代价

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 的无效唤醒少得多,但每次交接仍要付出系统调用的代价。


SectionSection第 11 节

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。

Figure 7 — Priority Inversion: The I/O Thread Waitsचित्र 7 — Priority Inversion: I/O Thread इंतज़ार करता है图 7 — 优先级反转:I/O 线程在等待
Priority inversion: the I/O thread waits for the GILCPUI/Ocrunching numbers… holds the GILsleeping📨 packet arrivesready… but waiting for the GILrunhigh-priority I/O waits behind low-priority CPU workज़रूरी I/O कम-ज़रूरी CPU काम के पीछे इंतज़ार करता है高优先级的 I/O 排在低优先级的 CPU 工作后面

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 并不在乎操作系统的优先级。

🚨 What happensक्या होता है发生了什么
01
The CPU-bound thread runs on Core 1 and holds the GIL.
CPU-bound thread Core 1 पर चलता है और GIL रखे है।
CPU 密集线程在核心 1 上运行并持有 GIL。
02
A packet arrives. The OS wakes the I/O thread on Core 2 and it asks for the GIL.
Packet आता है। OS Core 2 पर I/O thread को जगाता है और वह GIL माँगता है।
数据包到达。操作系统在核心 2 上唤醒 I/O 线程,它请求 GIL。
03
The GIL is busy. On Python 3 the I/O thread waits up to one switch interval (5 ms), then asks the CPU thread to drop it.
GIL व्यस्त है। Python 3 पर I/O thread एक switch interval (5 ms) तक इंतज़ार करता है, फिर CPU thread से उसे छोड़ने को कहता है।
GIL 正忙。在 Python 3 上,I/O 线程最多等一个切换间隔(5 毫秒),然后要求 CPU 线程释放。
04
The I/O thread runs a tiny bit of work, starts the next I/O, releases the GIL — and the CPU thread takes it back at once. Next packet: wait again.
I/O thread थोड़ा-सा काम करता है, अगला I/O शुरू करता है, GIL छोड़ता है — और CPU thread उसे तुरंत वापस ले लेता है। अगला packet: फिर इंतज़ार।
I/O 线程做一点点工作,开始下一次 I/O,释放 GIL — CPU 线程立刻拿回。下一个数据包:再等一次。
05
Every I/O operation pays this extra delay. Many small I/O operations add up: a web server or GUI feels slow even though the CPU work is "just" a background task.
हर I/O operation यह अतिरिक्त देरी चुकाता है। कई छोटे I/O operations जुड़ते जाते हैं: web server या GUI धीमा लगता है, भले ही CPU काम "बस" एक background task हो।
每次 I/O 操作都要付出这个额外延迟。许多小的 I/O 操作累加起来:即使 CPU 工作"只是"后台任务,Web 服务器或 GUI 也会显得迟钝。

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")
OUTPUT
CPU thread running: False avg wake-up 1.13 ms worst 1.21 ms CPU thread running: True avg wake-up 6.26 ms worst 6.69 ms
🔧
Fixed: On Python 3 the Wait Is About One Switch Interval — Per I/Oसुधार: Python 3 पर इंतज़ार लगभग एक Switch Interval है — हर I/O पर已修正:在 Python 3 上,每次 I/O 约等待一个切换间隔

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 毫秒时变长。

⚠️
Keep Heavy CPU Work Away From Latency-Sensitive I/Oभारी CPU काम को जल्दी-जवाब वाले I/O से दूर रखिए让繁重的 CPU 工作远离对延迟敏感的 I/O

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 工作移到单独的进程中。


SectionSection第 12 节

Signals, Ctrl-C and Threaded Programs ⌨️Signals, Ctrl-C और Threaded Programs ⌨️信号、Ctrl-C 与多线程程序 ⌨️

Why Ctrl-C Doesn't Stop Your Threaded ProgramCtrl-C आपका Threaded Program क्यों नहीं रोकता为什么 Ctrl-C 停不下你的多线程程序
You start a threaded Python program. It hangs. You press Ctrl-C. Nothing seems to happen. You press it again. Eventually you open another terminal and kill -9 it.

This is a classic annoyance, and it comes from how signals work with threads.
आप एक threaded Python program शुरू करते हैं। वह अटक जाता है। आप Ctrl-C दबाते हैं। कुछ होता नहीं दिखता। आप फिर दबाते हैं। आख़िर में आप दूसरा terminal खोलकर उसे kill -9 करते हैं।

यह एक पुरानी झुंझलाहट है, और यह signals के threads के साथ काम करने के तरीक़े से आती है।
你启动一个多线程 Python 程序,它卡住了。你按下 Ctrl-C,好像什么也没发生。你再按一次。最后你只好打开另一个终端,用 kill -9 杀掉它。

这是一个经典的烦恼,源于信号与线程的交互方式。
👥 Why signals get weirdSignals अजीब क्यों होते हैं信号为何变得古怪
Fact 1
Python signal handlers (like the one that raises KeyboardInterrupt) only run in the main thread.
Python signal handlers (जैसे KeyboardInterrupt raise करने वाला) सिर्फ़ main thread में चलते हैं।
Python 的信号处理器(比如抛出 KeyboardInterrupt 的那个)只在 主线程 中运行。
Fact 2
Ctrl-C only marks a pending signal. The handler runs when the main thread next gets to run Python code.
Ctrl-C सिर्फ़ एक pending signal दर्ज करता है। Handler तब चलता है जब main thread अगली बार Python code चला पाता है।
Ctrl-C 只是 标记 一个待处理的信号。处理器要等主线程下一次运行 Python 代码时才执行。
Fact 3
Worker threads are not interrupted. A non-daemon worker that never stops keeps the program alive, because Python waits for it at exit.
Worker threads interrupt नहीं होते। कभी न रुकने वाला non-daemon worker program को ज़िंदा रखता है, क्योंकि exit पर Python उसका इंतज़ार करता है।
工作线程不会被中断。一个永不停止的非守护工作线程会让程序一直活着,因为 Python 退出时会等它。
Result
Main may get KeyboardInterrupt, but the program still does not exit until the workers stop.
Main को 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
OUTPUT
main: waiting on t.join() ... ^C <- 1st Ctrl-C (after 1 s) Traceback (most recent call last): File "demo.py", line 10, in <module> t.join() # bare join, no timeout ~~~~~~^^ File "/usr/lib/python3.13/threading.py", line 1095, in join self._handle.join(timeout) ~~~~~~~~~~~~~~~~~^^^^^^^^^ KeyboardInterrupt [1.5 s later the program is STILL running - the worker never stopped] ^C <- 2nd Ctrl-C (Python stops waiting for the thread at exit) Traceback (most recent call last): File "/usr/lib/python3.13/threading.py", line 1543, in _shutdown _thread_shutdown() KeyboardInterrupt:
🔧
Fixed: A Bare join() Is Interrupted on Python 3 (Linux/macOS) — the Worker Is the Problemसुधार: Python 3 (Linux/macOS) पर Bare join() Interrupt होता है — असली समस्या Worker है已修正:在 Python 3(Linux/macOS)上,裸 join() 是可以被中断的 — 问题出在工作线程

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()
OUTPUT (Ctrl-C sent after 1.2 s)OUTPUT (1.2 s बाद Ctrl-C भेजा)输出(1.2 秒后发送 Ctrl-C)
^C <- Ctrl-C (after 1.2 s) Ctrl-C received - asking worker to stop Worker exiting cleanly.
💡
The Fixइलाज解决方法

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() 不可中断的平台上也有帮助。


SectionSection第 13 节

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")
OUTPUT (our 2-core machine)OUTPUT (हमारी 2-core machine)输出(我们的双核机器)
Single thread: 1.28s Two threads: 1.32s Two processes: 0.70s
🔧
Fixed: Threads Were Only a Little Slower, and the Script Needs a __main__ Guardसुधार: Threads थोड़े ही धीमे थे, और Script को __main__ Guard चाहिए已修正:线程只是稍慢一点,而且脚本需要 __main__ 保护

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 上会失败,因为那里的子进程会重新导入脚本。


SectionSection第 14 节

Threads Shine for I/O — Same Benchmark, Different Work 🌐Threads I/O में चमकते हैं — वही Benchmark, अलग काम 🌐线程在 I/O 上大放异彩 — 同样的测试,不同的工作 🌐

ℹ️
About fake_get()fake_get() के बारे में关于 fake_get()

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")
OUTPUT
Sequential: 10.00s 10 threads: 1.00s

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 工作。


SectionSection第 15 节

Enter the Queue — Thread-Safe Communication 📨Queue आता है — Thread-Safe बातचीत 📨队列登场 — 线程安全的通信 📨

Figure 8 — Producer-Consumer Flow With queue.Queueचित्र 8 — queue.Queue के साथ Producer-Consumer Flow图 8 — 使用 queue.Queue 的生产者-消费者流程
Producer-consumer flow with queue.QueueProducer 1Producer 2put()#5#4#3#2#1queue.Queue (FIFO, locks inside)queue.Queue (FIFO, अंदर locks)queue.Queue(先进先出,内部加锁)Consumer 1Consumer 2Consumer 3get()

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() 取出。所有加锁都在队列内部完成。

The Restaurant KitchenRestaurant की रसोई餐厅厨房
In a busy restaurant, waiters (producers) do not hand orders straight to chefs. They pin tickets to a rail (the queue). Chefs (consumers) take the next ticket when they are free. Nobody shouts across the kitchen, and if the kitchen is overloaded the rail fills up, so the waiters slow down.

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.
व्यस्त restaurant में waiters (producers) orders सीधे chefs को नहीं देते। वे tickets एक rail (queue) पर टाँग देते हैं। Chefs (consumers) ख़ाली होने पर अगला ticket ले लेते हैं। कोई रसोई में चिल्लाता नहीं, और अगर रसोई पर बोझ ज़्यादा हो तो rail भर जाती है, इसलिए waiters धीमे हो जाते हैं।

Python का queue.Queue वही rail है: एक thread-safe, first-in-first-out buffer जो producer threads से consumer threads तक काम पहुँचाता है, बिना आपके एक भी Lock लिखे।
在繁忙的餐厅里,服务员(生产者)不会把订单直接交给厨师。他们把单子挂在一根横杆上(队列)。厨师(消费者)空闲时就取下一张单子。没有人隔着厨房大喊,如果厨房忙不过来,横杆上的单子就会堆满,服务员自然会放慢。

Python 的 queue.Queue 就是那根横杆:一个线程安全、先进先出的缓冲区,把工作从生产者线程交给消费者线程,你一行 Lock 都不用写。
📨
queue.Queue
FIFO — first in, first outFIFO — पहले आया, पहले गयाFIFO — 先进先出
The default. Items come out in the order they went in. Perfect for work queues where every task is handled in order.
Default. Items उसी क्रम में निकलते हैं जिसमें डाले गए। ऐसी work queues के लिए बढ़िया जहाँ हर task क्रम से संभाला जाए।
默认类型。元素按放入的顺序取出。非常适合按顺序处理每个任务的工作队列。
📚
queue.LifoQueue
LIFO — last in, first outLIFO — आख़िरी आया, पहले गयाLIFO — 后进先出
A stack. Useful when the newest item matters most — for example an undo history or newest-first notifications.
एक stack। तब उपयोगी जब सबसे नया item सबसे ज़रूरी हो — जैसे undo history या सबसे-नया-पहले notifications।
一个栈。适合最新元素最重要的场景 — 例如撤销历史或最新优先的通知。
🏆
queue.PriorityQueue
lowest number firstसबसे छोटा number पहले数字最小的先出
Items are tuples like (priority, data). The lowest priority number comes out first. Good for schedulers where urgent work jumps the line.
Items (priority, data) जैसे tuples हैं। सबसे छोटा priority number पहले निकलता है। Schedulers के लिए अच्छा जहाँ ज़रूरी काम कतार तोड़कर आगे जाए।
元素是 (priority, data) 这样的元组。优先级数字最小的先出。适合紧急任务插队的调度器。
🔑
Why Not Just Use a List?बस एक List क्यों नहीं?为什么不直接用列表?

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() 总是安全的,而且能替你 等待。你永远不需要在它外面自己加锁。


SectionSection第 16 节

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 个随机序列一致。

Queue PlaygroundQueue PlaygroundQueue 游乐场checked on 300 real sequences300 असली sequences पर जाँचा已用 300 个真实序列验证
Queue:Queue:队列:
CodeCode代码
Resultsनतीजे结果

SectionSection第 17 节

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.")
OUTPUT (order varies from run to run)OUTPUT (क्रम हर run में बदलता है)输出(每次运行顺序不同)
[P P0] produced P0-item-0 (queue size=1)[P P1] produced P1-item-0 (queue size=2) [C C0] consumed P0-item-0 [C C1] consumed P1-item-0 [P P0] produced P0-item-1 (queue size=1) [C C2] consumed P0-item-1 [P P1] produced P1-item-1 (queue size=1) [C C0] consumed P1-item-1 [P P0] produced P0-item-2 (queue size=1) [P P1] produced P1-item-2 (queue size=2) [P P0] produced P0-item-3 (queue size=3) [C C1] consumed P0-item-2 [C C2] consumed P1-item-2 [P P1] produced P1-item-3 (queue size=2) ... (25 more lines) [C C0] consumed P0-item-9 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,通知它停止。

💊
The Poison Pill PatternPoison Pill Pattern毒丸模式

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 无法从外部杀死一个线程)。


SectionSection第 18 节

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")
OUTPUT
[W0] https://httpbin.org/delay/1?id=0 -> 200 [W2] https://httpbin.org/delay/1?id=2 -> 200 [W1] https://httpbin.org/delay/1?id=1 -> 200[W4] https://httpbin.org/delay/1?id=4 -> 200[W5] https://httpbin.org/delay/1?id=5 -> 200 [W3] https://httpbin.org/delay/1?id=3 -> 200 [W6] https://httpbin.org/delay/1?id=6 -> 200 ... (43 more lines) [W7] https://httpbin.org/delay/1?id=45 -> 200 Fetched 50 URLs in 5.00s using 10 threads Sequential would take ~50s

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 传递、而不是靠打印文本的原因。


SectionSection第 19 节

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 पहले निकलता है।

一个支持工单系统,紧急工单可以插队。优先级数字放在最前面;数字最小 的先出。

Figure 9 — PriorityQueue Reorders Itemsचित्र 9 — PriorityQueue Items का क्रम बदलता है图 9 — PriorityQueue 重新排列元素
PriorityQueue reordersput() ORDERput() का क्रमput() 的顺序get() ORDERget() का क्रमget() 的顺序P3 typoP1 PROD DOWNP5 copyrightP2 loginP1 breachheapsmallest on topसबसे छोटा ऊपर最小的在顶部1. P1 PROD DOWN2. P1 breach3. P2 login4. P3 typo5. P5 copyright

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()
OUTPUT — sorted by priority, not by put() orderOUTPUT — priority से क्रम, put() क्रम से नहीं输出 — 按优先级排序,而不是 put() 的顺序
[P1] PROD DOWN - payment gateway failing! [P1] Data breach alert! [P2] Enterprise customer login broken [P3] Fix typo on homepage [P5] Update the copyright year
🔧
Fixed: Equal Priorities Are Not Guaranteed to Come Out in put() Orderसुधार: बराबर Priorities का put() क्रम में निकलना पक्का नहीं已修正:相同优先级的元素不保证按 put() 顺序取出

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!
OUTPUT
Traceback (most recent call last): File "demo.py", line 5, in <module> pq.put((1, {"task": "deploy"})) # same priority -> compares the dicts! ~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.13/queue.py", line 173, in put self._put(item) ~~~~~~~~~^^^^^^ File "/usr/lib/python3.13/queue.py", line 293, in _put heappush(self.queue, item) ~~~~~~~~^^^^^^^^^^^^^^^^^^ TypeError: '<' not supported between instances of 'dict' and 'dict'
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}")
OUTPUT
[P1] #1 PROD DOWN - payment gateway failing! [P1] #4 Data breach alert! [P1] #5 {'task': 'a dict is fine now'} [P2] #3 Enterprise customer login broken [P3] #0 Fix typo on homepage [P5] #2 Update the copyright year

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 次真实运行核对。

PriorityQueue LabPriorityQueue LabPriorityQueue 实验室checked on 300 real sequences300 असली sequences पर जाँचा已用 300 个真实序列验证
Style:तरीक़ा:写法:
CodeCode代码
LogLog日志

SectionSection第 20 节

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 是一个内含队列、接口简洁的线程池。它替你管理队列、工作线程和结果。对大多数新的批处理任务,首选它。

Figure 10 — Inside ThreadPoolExecutorचित्र 10 — ThreadPoolExecutor के अंदर图 10 — ThreadPoolExecutor 内部
ThreadPoolExecutor internalsyour codesubmit(fn, x)ThreadPoolExecutorwork queue(SimpleQueue)worker 0worker 1worker 2Futures.result()

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")
OUTPUT
200 308B https://httpbin.org/delay/1?id=0 200 308B https://httpbin.org/delay/1?id=1 200 308B https://httpbin.org/delay/1?id=2 200 308B https://httpbin.org/delay/1?id=5 200 308B https://httpbin.org/delay/1?id=3 200 308B https://httpbin.org/delay/1?id=6 ... (13 more lines) 200 308B https://httpbin.org/delay/1?id=19 Done in 2.00s
🏆
Executor vs Raw Queue — When to Use WhichExecutor बनाम सीधी Queue — कब कौन-साExecutor 与原始队列 — 何时用哪个

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 放回队列)。


SectionSection第 21 节

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 का अपना समूह दीजिए।

真实系统常常分成几个阶段:下载 → 解析 → 保存。在阶段之间放一个队列,并给每个阶段配一组自己的线程。

Figure 11 — Three-Stage Pipeline With Chained Queuesचित्र 11 — जुड़ी Queues वाली तीन-Stage Pipeline图 11 — 用串联队列构成的三阶段流水线
Three-stage pipelineDOWNLOADDOWNLOAD下载w0w1w2w3parse_qPARSEPARSE解析w0w1save_qSAVESAVE保存w0slow I/O stage gets the most workersधीमे I/O stage को सबसे ज़्यादा workers最慢的 I/O 阶段分配最多的工作线程

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.")
OUTPUT
SAVED {'url': 'https://example.com/page0', 'len': 43} SAVED {'url': 'https://example.com/page3', 'len': 43} SAVED {'url': 'https://example.com/page2', 'len': 43} SAVED {'url': 'https://example.com/page1', 'len': 43} SAVED {'url': 'https://example.com/page4', 'len': 43} SAVED {'url': 'https://example.com/page5', 'len': 43} SAVED {'url': 'https://example.com/page6', 'len': 43} SAVED {'url': 'https://example.com/page7', 'len': 43} SAVED {'url': 'https://example.com/page10', 'len': 44} SAVED {'url': 'https://example.com/page8', 'len': 43} SAVED {'url': 'https://example.com/page11', 'len': 44} SAVED {'url': 'https://example.com/page9', 'len': 43} SAVED {'url': 'https://example.com/page12', 'len': 44} SAVED {'url': 'https://example.com/page13', 'len': 44} SAVED {'url': 'https://example.com/page15', 'len': 44} SAVED {'url': 'https://example.com/page14', 'len': 44} SAVED {'url': 'https://example.com/page16', 'len': 44} SAVED {'url': 'https://example.com/page17', 'len': 44} SAVED {'url': 'https://example.com/page18', 'len': 44} SAVED {'url': 'https://example.com/page19', 'len': 44} Pipeline complete in 1.70s.
🔧
Fixed: One Saver Does Not Keep the Input Orderसुधार: एक Saver Input का क्रम नहीं रखता已修正:一个保存线程并不能保持输入顺序

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 秒)。选择启动多少个下载线程和解析线程。每个条形表示某个工作线程处理的一个页面,来自真实运行。

Pipeline LabPipeline Lab流水线实验室6 real runs6 असली runs6 次真实运行
Downloaders:Downloaders:下载线程:
Parsers:Parsers:解析线程:
🛠️
Tune Worker Counts per Stageहर Stage के Workers ठीक कीजिए按阶段调整工作线程数

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 阶段(下载)分配最多的工作线程。保存只用一个线程,这样写入文件或数据库永远不会冲突。让每个阶段的线程数与其瓶颈相匹配 — 流水线实验室展示了不这样做的后果。


SectionSection第 22 节

Getting Around the GIL — Your Options 🚀GIL से बचने के रास्ते — आपके विकल्प 🚀绕过 GIL — 你的选项 🚀

📡
multiprocessing
CPU-bound → number crunchingCPU-bound → गणनाCPU 密集 → 数值计算
Each process has its own interpreter and its own GIL, so Python code runs in true parallel on several cores. Use multiprocessing.Pool or ProcessPoolExecutor for CPU-bound work.
हर process का अपना interpreter और अपना GIL है, इसलिए Python code कई cores पर सच में parallel चलता है। CPU-bound काम के लिए multiprocessing.Pool या ProcessPoolExecutor लीजिए।
每个进程都有自己的解释器和 GIL,所以 Python 代码能在多个核心上真正并行。CPU 密集型工作请用 multiprocessing.Pool 或 ProcessPoolExecutor。
🔁
asyncio
I/O at very high concurrencyबहुत ज़्यादा concurrency वाला I/O超高并发的 I/O
Cooperative multitasking on one thread. Handles thousands of connections without one OS thread each — good for things like websocket servers. Needs async/await code.
एक thread पर cooperative multitasking। हर connection के लिए अलग OS thread के बिना हज़ारों connections संभालता है — websocket servers जैसी चीज़ों के लिए अच्छा। async/await code चाहिए।
在单线程上进行协作式多任务。无需为每个连接开一个操作系统线程就能处理成千上万个连接 — 适合 websocket 服务器之类。需要 async/await 代码。
🔥
C Extensions (NumPy, Cython)C Extensions (NumPy, Cython)C 扩展(NumPy、Cython)
vectorised numeric codevectorised numeric code向量化数值代码
C code can release the GIL during heavy work. Many NumPy operations do; large matrix products like np.dot may even use several cores, depending on the BLAS library NumPy was built with.
C code भारी काम के दौरान GIL छोड़ सकता है। कई NumPy operations ऐसा करते हैं; np.dot जैसे बड़े matrix products कई cores भी इस्तेमाल कर सकते हैं, यह NumPy के BLAS library पर निर्भर है।
C 代码可以在繁重计算期间释放 GIL。许多 NumPy 操作都会这样做;像 np.dot 这样的大矩阵乘法甚至可能用到多个核心,取决于 NumPy 所链接的 BLAS 库。
🧠
Free-threaded PythonFree-threaded Python自由线程 Python
official since 3.14, opt-in3.14 से आधिकारिक, opt-in3.14 起官方支持,需主动选择
A separate build without the GIL (PEP 703; officially supported since 3.14 by PEP 779). Threads run Python code in true parallel. Not the default build, and some C extensions are not ready yet.
GIL के बिना एक अलग build (PEP 703; PEP 779 द्वारा 3.14 से आधिकारिक रूप से supported)। Threads Python code सच में parallel चलाते हैं। Default build नहीं, और कुछ C extensions अभी तैयार नहीं।
一个没有 GIL 的独立版本(PEP 703;自 3.14 起由 PEP 779 正式支持)。线程能真正并行运行 Python 代码。它不是默认版本,部分 C 扩展还未适配。
🏗️
SubinterpretersSubinterpreters子解释器
advanced, still youngउन्नत, अभी नया进阶,仍较新
Since 3.12 (PEP 684) each subinterpreter in one process can have its own GIL; Python 3.14 added the concurrent.interpreters module to use them. A middle ground between threads and processes.
3.12 (PEP 684) से एक process में हर subinterpreter का अपना GIL हो सकता है; Python 3.14 ने उन्हें इस्तेमाल करने के लिए concurrent.interpreters module जोड़ा। Threads और processes के बीच का रास्ता।
自 3.12(PEP 684)起,同一进程中的每个子解释器都可以有自己的 GIL;Python 3.14 新增了 concurrent.interpreters 模块来使用它们。介于线程和进程之间的折中方案。
🚀
Other runtimesदूसरे runtimes其他运行时
niche — check compatibilityसीमित — compatibility जाँचें小众 — 注意兼容性
Jython and IronPython have no GIL (they use the JVM / .NET threads) but lag behind modern Python. PyPy has a GIL but runs pure Python much faster. Rare in production today.
Jython और IronPython में GIL नहीं (वे JVM / .NET threads इस्तेमाल करते हैं) पर वे आधुनिक Python से पीछे हैं। PyPy में GIL है पर वह pure Python बहुत तेज़ चलाता है। आज production में कम मिलते हैं।
Jython 和 IronPython 没有 GIL(它们用 JVM / .NET 的线程),但落后于现代 Python。PyPy 有 GIL,但运行纯 Python 快得多。如今在生产中很少见。
🔧
Fixed: Updated Facts on Free-Threading and Subinterpretersसुधार: Free-Threading और Subinterpreters पर नए तथ्य已修正:关于自由线程和子解释器的最新事实

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 库。


SectionSection第 23 节

Common Pitfalls and Fixes 🩺आम ग़लतियाँ और उनके Fix 🩺常见陷阱与修复 🩺

⚠️ Threading + Queue — bugs you will hitThreading + Queue — bugs जो आपको मिलेंगे线程 + 队列 — 你一定会遇到的 bug
Bug 1
Forgetting 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。
Bug 2
No shutdown signal → consumers wait on get() for ever and the program never ends. Send one poison pill (None) per consumer.
कोई shutdown signal नहीं → consumers get() पर हमेशा इंतज़ार करते हैं और program कभी ख़त्म नहीं होता। हर consumer के लिए एक poison pill (None) भेजिए।
没有关闭信号 → 消费者永远在 get() 上等待,程序永远不结束。给每个消费者发送一个毒丸(None)。
Bug 3
Unbounded queue → producers outrun consumers and memory fills up. Use Queue(maxsize=1000) so producers wait (backpressure).
असीमित queue → producers consumers से आगे निकल जाते हैं और memory भर जाती है। Queue(maxsize=1000) लीजिए ताकि producers रुकें (backpressure)।
无界队列 → 生产者比消费者快,内存被填满。使用 Queue(maxsize=1000),让生产者等待(背压)。
Bug 4
Shared mutable state → threads change the same dict or list without a lock. Pass data through the queue instead of shared globals.
साझा बदलने वाली state → threads बिना lock के एक ही dict या list बदलते हैं। साझा globals की जगह data queue से भेजिए।
共享可变状态 → 多个线程不加锁地修改同一个字典或列表。通过队列传递数据,而不是用共享的全局变量。
Bug 5
Program never exits → a non-daemon worker is still looping, and Python waits for it at exit. Stop workers with a poison pill or event and join() them; use daemon=True only for workers that hold nothing important.
Program कभी exit नहीं होता → कोई non-daemon worker अब भी loop कर रहा है, और exit पर Python उसका इंतज़ार करता है। Workers को poison pill या event से रोकिए और join() कीजिए; daemon=True सिर्फ़ उन workers के लिए जो कुछ ज़रूरी नहीं रखते।
程序永不退出 → 某个非守护工作线程仍在循环,Python 退出时会等它。用毒丸或事件停止工作线程并 join() 它们;daemon=True 只用于不保存重要数据的工作线程。
Bug 6
A crashed worker goes unnoticed → Python prints a traceback to stderr, but the thread is dead, its item never got task_done(), and main does not know. Catch exceptions in the worker and send them to an error queue.
Crash हुआ worker किसी को पता नहीं चलता → Python stderr पर traceback print करता है, पर thread मर चुका है, उसके item का task_done() नहीं हुआ, और main को पता नहीं। Worker में exceptions पकड़िए और उन्हें error queue में भेजिए।
崩溃的工作线程无人察觉 → Python 会在 stderr 打印一条回溯,但线程已经死了,它的元素没有得到 task_done(),主线程也不知道。在工作线程中捕获异常,把它们送进错误队列。
Bug 7
Ctrl-C does not stop the program → workers never stop. Give them a stop event or poison pill, and wait with t.join(timeout=0.5) in a loop.
Ctrl-C program नहीं रोकता → workers कभी नहीं रुकते। उन्हें stop event या poison pill दीजिए, और loop में t.join(timeout=0.5) से इंतज़ार कीजिए।
Ctrl-C 停不下程序 → 工作线程永不停止。给它们一个停止事件或毒丸,并在循环中用 t.join(timeout=0.5) 等待。
🔧
Fixed: Exceptions in Threads Are Not Silentसुधार: Threads में Exceptions चुप नहीं होते已修正:线程中的异常并不是悄无声息的

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() 永远等待,主线程也永远不知道出了错。

✅
Safe Worker Templateसुरक्षित Worker Template安全的工作线程模板

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))
OUTPUT
processed a processed b errors: [('bad', 'ValueError("cannot process \'bad\'")')]

🩺 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 秒超时进行检查)。

Bug DoctorBug DoctorBug 医生6 bugs · real outputs6 bugs · असली outputs6 个 bug · 真实输出
❌ BuggyBuggy有 bug
✅ Fixedठीक किया已修正

SectionSection第 24 节

Threads vs Processes vs asyncio — Decision Table ⚖️Threads बनाम Processes बनाम asyncio — फ़ैसले की Table ⚖️线程 vs 进程 vs asyncio — 决策表 ⚖️

Propertyगुण特性threading + Queuemultiprocessingasyncio
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一个完整的解释器 — 常为数十 MBA 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)较高(协作式调度)
🔧
Fixed: "~8 MB per Thread" Is Reserved Address Space, Not Used Memoryसुधार: "~8 MB प्रति Thread" Reserved Address Space है, इस्तेमाल हुई Memory नहीं已修正:"每线程约 8 MB"是预留的地址空间,而不是实际占用的内存

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 线程实际占用的内存要少得多。线程很便宜,几百个也没问题。真正的限制通常是远端服务器和连接池,而不是内存。


SectionSection第 25 节

Golden Rules and Quiz 🏆सुनहरे नियम और Quiz 🏆黄金法则与小测验 🏆

🔑 GIL & Threading With Queue — Rules to RememberGIL और Queue के साथ Threading — याद रखने वाले नियमGIL 与基于队列的多线程 — 必须记住的规则
1
Do not use threads to speed up CPU-bound Python code. The GIL lets only one run Python at a time, so you get no speed-up (on Python 2 it was even 1.8× slower). Use multiprocessing or a C extension like NumPy.
CPU-bound Python code तेज़ करने के लिए threads मत लीजिए। GIL एक समय में सिर्फ़ एक को Python चलाने देता है, इसलिए कोई speed-up नहीं (Python 2 पर तो 1.8× धीमा भी था)। multiprocessing या NumPy जैसा C extension लीजिए।
不要用线程来加速 CPU 密集的 Python 代码。GIL 一次只让一个线程运行 Python,所以不会有任何加速(在 Python 2 上甚至慢 1.8 倍)。请使用 multiprocessing 或 NumPy 这样的 C 扩展。
2
Do use threads for I/O-bound work — network calls, disk reads, database queries. The GIL is released while a thread waits, so the waits overlap.
I/O-bound काम के लिए threads ज़रूर लीजिए — network calls, disk reads, database queries। Thread के इंतज़ार के दौरान GIL छूटता है, इसलिए इंतज़ार साथ-साथ होते हैं।
对 I/O 密集型工作一定要用线程 — 网络调用、磁盘读取、数据库查询。线程等待时会释放 GIL,所以等待可以重叠。
3
Do not mix heavy CPU work and latency-sensitive I/O in one process. Each I/O operation can wait about one switch interval (5 ms) for the GIL. Move the CPU work to another process.
एक process में भारी CPU काम और जल्दी-जवाब वाला I/O मत मिलाइए। हर I/O operation GIL के लिए लगभग एक switch interval (5 ms) इंतज़ार कर सकता है। CPU काम दूसरे process में ले जाइए।
不要在同一个进程中混合繁重的 CPU 工作和对延迟敏感的 I/O。每次 I/O 操作都可能为 GIL 等上约一个切换间隔(5 毫秒)。把 CPU 工作移到另一个进程。
4
Pass data between threads through queue.Queue, not through shared mutable objects. The queue's internal locks remove most race conditions.
Threads के बीच data queue.Queue से भेजिए, साझा बदलने वाले objects से नहीं। Queue के अंदर के locks ज़्यादातर race conditions हटा देते हैं।
线程之间通过 queue.Queue 传递数据,而不是通过共享的可变对象。队列内部的锁能消除绝大多数竞态条件。
5
Pair every 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() 会永远等待。
6
Set maxsize on queues in long-running pipelines. An unbounded queue fills memory when producers outrun consumers.
लंबी चलने वाली pipelines की queues पर maxsize set कीजिए। Producers के consumers से आगे निकलने पर असीमित queue memory भर देती है।
长期运行的流水线中的队列要设置 maxsize。生产者比消费者快时,无界队列会占满内存。
7
Shut consumers down with poison pills — one None per worker after the real work is done. Do not rely on daemon=True alone to stop workers that hold state.
Consumers को poison pills से बंद कीजिए — असली काम के बाद हर worker के लिए एक None। State रखने वाले workers को रोकने के लिए सिर्फ़ daemon=True पर भरोसा मत कीजिए।
用毒丸关闭消费者 — 真正的工作完成后,每个工作线程一个 None。不要只依靠 daemon=True 来停止持有状态的工作线程。
8
Make sure Ctrl-C can stop your program: give workers a stop event or poison pill, and wait with t.join(timeout=0.5) in a loop.
पक्का कीजिए कि Ctrl-C आपका program रोक सके: workers को stop event या poison pill दीजिए, और loop में t.join(timeout=0.5) से इंतज़ार कीजिए।
确保 Ctrl-C 能停止你的程序:给工作线程一个停止事件或毒丸,并在循环中用 t.join(timeout=0.5) 等待。
9
For most batch jobs, prefer concurrent.futures.ThreadPoolExecutor over hand-made thread + queue code. It manages the queue, the pool and the results in a few lines.
ज़्यादातर batch jobs के लिए हाथ से बने thread + queue code की जगह concurrent.futures.ThreadPoolExecutor लीजिए। यह कुछ lines में queue, pool और नतीजे संभालता है।
对大多数批处理任务,优先使用 concurrent.futures.ThreadPoolExecutor,而不是手写的线程 + 队列代码。它用几行代码就管理好了队列、线程池和结果。
10
Never let exceptions vanish inside a worker. Catch them and send them to an error queue or a logger.
Worker के अंदर exceptions को कभी ग़ायब मत होने दीजिए। उन्हें पकड़िए और error queue या logger में भेजिए।
绝不要让异常在工作线程里消失。捕获它们并送到错误队列或日志中。
11
Match the worker count to the work. 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.
Worker count काम के हिसाब से रखिए। 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 工作常常可以用更多,但服务器的速率限制和连接池通常决定了有效的上限。
🔧
Fixed: The Worker-Count Formulaसुधार: Worker-Count Formula已修正:工作线程数公式

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 道题

🏆 QuizPick an answer to see why.जवाब चुनें और कारण देखें।选一个答案,看看原因。
🎯
Key Takeawayमुख्य सीख关键要点

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。

🐍 Try Python Yourself

Write, run and visualize Python step by step. No install needed.