Python Advance 📂 Threads · 4 of 4 309 min read

Python Daemon Threads, Synchronization, Lock & RLock

Master the trickiest parts of Python threading — daemon threads that die with the main program, race conditions that corrupt shared data, Lock for mutual exclusion, and RLock (reentrant lock) for functions that call themselves or nested locking. Loaded with runnable examples showing broken vs fixed code, deadlock demos, a bank-account transfer example, and SVG diagrams for every concept.

🌐
SectionSection第 01 节

The Story That Explains Synchronization 📝Synchronization समझाने वाली कहानी 📝一个讲清同步的故事 📝

Two People Editing One Document Without Taking Turnsदो लोग बिना बारी के एक Document बदलते हुए两个人不轮流地编辑同一份文档
Imagine you and a friend open the same document in an app without live sync. You both read the word count: 100. You add a paragraph and save 150. Meanwhile your friend, still seeing 100, adds a paragraph and saves 140. Whoever saves last wins — the other person's work vanishes.

This is a race condition: two workers reading and writing the same shared data with no coordination. In a Python program, nothing prevents it by default. Two threads doing counter += 1 at the same moment can end with counter = 1 instead of 2 — with no error message, just silently wrong data.

A Lock is Python's turn-taking token. Only the thread holding the lock may touch the shared data; everyone else waits. That is thread synchronization — letting threads share data without stepping on each other.
मान लीजिए आप और आपका दोस्त एक ही document ऐसे app में खोलते हैं जिसमें live sync नहीं है। दोनों word count पढ़ते हैं: 100। आप एक paragraph जोड़कर 150 save करते हैं। इसी बीच आपका दोस्त, जिसे अब भी 100 दिख रहा है, एक paragraph जोड़कर 140 save करता है। जो आख़िर में save करे वही जीतता है — दूसरे का काम ग़ायब।

यही race condition है: दो workers बिना तालमेल के एक ही साझा data पढ़ते और लिखते हैं। Python program में default रूप से इसे कोई नहीं रोकता। एक ही पल में counter += 1 करने वाले दो threads के अंत में 2 की जगह counter = 1 हो सकता है — कोई error message नहीं, बस चुपचाप ग़लत data।

Lock Python का बारी वाला token है। सिर्फ़ lock पकड़े thread को साझा data छूने की इजाज़त है; बाक़ी सब इंतज़ार करते हैं। यही thread synchronization है — threads को एक-दूसरे का काम बिगाड़े बिना data बाँटने देना।
想象你和朋友在一个 没有 实时同步的应用里打开同一份文档。你们都看到字数是 100。你加了一段,保存为 150。与此同时,你的朋友看到的还是 100,他也加了一段,保存为 140。谁最后保存谁就赢 — 另一个人的工作就消失了。

这就是 竞态条件:两个工作者在没有协调的情况下读写同一份共享数据。在 Python 程序中,默认没有任何东西能阻止它。两个线程同时执行 counter += 1,最后可能得到 counter = 1 而不是 2 — 没有报错,只是悄悄地得到了错误的数据。

锁(Lock) 就是 Python 的"轮流令牌"。只有持有锁的线程才能碰共享数据;其他线程都要等待。这就是 线程同步 — 让线程共享数据而不互相干扰。

This tutorial covers two everyday tools for safe threading: daemon threads (background helpers that stop when the program exits) and the two lock types — Lock and RLock — that stop threads from corrupting shared data.

यह tutorial सुरक्षित threading के दो रोज़ के औज़ार बताता है: daemon threads (background सहायक जो program ख़त्म होने पर रुक जाते हैं) और दो तरह के locks — Lock और RLock — जो threads को साझा data बिगाड़ने से रोकते हैं।

本教程介绍安全多线程的两类常用工具:守护线程(daemon threads)(程序退出时随之停止的后台助手),以及两种锁 — Lock 和 RLock — 它们能防止线程破坏共享数据。

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

The moment two or more threads read and write the same piece of data, you have a synchronization problem. Locks solve it by turning many overlapping writes into a single-file line where each thread waits its turn.

जिस पल दो या ज़्यादा threads एक ही data को पढ़ते और लिखते हैं, आपके पास synchronization की समस्या है। Locks कई एक-साथ होने वाले writes को एक कतार में बदलकर इसे हल करते हैं, जहाँ हर thread अपनी बारी का इंतज़ार करता है।

只要两个或更多线程既读 又 写同一份数据,你就遇到了同步问题。锁通过把许多重叠的写操作变成单列队伍来解决它,每个线程都要等轮到自己。

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

Look for the interactive boxes: the Daemon Timeline (Section 02), the Race Stepper and Race Lab (03), the Deadlock Lab (05), the Lock vs RLock Playground (06) and the Bug Doctor (09). Their outputs come from real runs.

Interactive डिब्बे खोजें: Daemon Timeline (Section 02), Race Stepper और Race Lab (03), Deadlock Lab (05), Lock बनाम RLock Playground (06) और Bug Doctor (09)। इनके outputs असली runs से हैं।

请留意这些互动框:守护线程时间线(第 02 节)、竞态单步演示 和 竞态实验室(03)、死锁实验室(05)、Lock vs RLock 游乐场(06)以及 Bug 医生(09)。它们的输出都来自真实运行。


SectionSection第 02 节

Daemon Threads — Background Helpers That Stop at Exit 👻Daemon Threads — Exit पर रुकने वाले Background सहायक 👻守护线程 — 程序退出时随之停止的后台助手 👻

Every thread is either a non-daemon thread (the default) or a daemon thread. The difference only shows when the program shuts down:

हर thread या तो non-daemon thread (default) है या daemon thread। फ़र्क़ सिर्फ़ program बंद होते समय दिखता है:

每个线程要么是 非守护 线程(默认),要么是 守护 线程。区别只在程序关闭时才显现:

👤 Non-daemon (default)Non-daemon (default)非守护(默认)
Python waits for it to finish before exitingExit से पहले Python इसके ख़त्म होने का इंतज़ार करता है退出前 Python 会等它结束
Use for work that MUST completeउस काम के लिए जो पूरा होना ही चाहिए用于必须完成的工作
e.g. saving user data, flushing logsजैसे user data save करना, logs flush करना例如保存用户数据、刷新日志
daemon=False (or not set)daemon=False (या set नहीं)daemon=False(或不设置)
👻 DaemonDaemon守护线程
Stopped abruptly when the program exitsProgram exit होने पर अचानक रोक दिया जाता है程序退出时被突然停止
Use for background monitors, heartbeats, watchersBackground monitors, heartbeats, watchers के लिए用于后台监控、心跳、观察者
e.g. a metrics ticker, a cache-expiry loopजैसे metrics ticker, cache-expiry loop例如指标计时器、缓存过期循环
daemon=True — set BEFORE start()daemon=True — start() से पहलेdaemon=True — 在 start() 之前设置
Figure 1 — What Happens When Main Exitsचित्र 1 — Main के Exit होने पर क्या होता है图 1 — 主线程退出时发生了什么
What happens when main exitsNON-DAEMON — the program WAITSNON-DAEMON — program इंतज़ार करता है非守护 — 程序会等待mainmain codeworkerstill running… → done↑ program exits here(after the worker)DAEMON — stopped with the programDAEMON — program के साथ रुका守护 — 随程序一起停止mainmain codedaemonrunning…never runs ✗↑ program exits here — the daemon is cut off

Non-daemon threads keep the program alive. Daemon threads do not — they are stopped as soon as the last non-daemon thread finishes.

Non-daemon threads program को ज़िंदा रखते हैं। Daemon threads नहीं — आख़िरी non-daemon thread ख़त्म होते ही वे रोक दिए जाते हैं।

非守护线程会让程序保持运行。守护线程不会 — 最后一个非守护线程一结束,它们就被停止。

Creating a daemon threadDaemon thread बनाना创建守护线程

import threading, time

def background_monitor():
    # Infinite loop - would never let a non-daemon program exit
    while True:
        time.sleep(1)
        print("[monitor] heartbeat")

# Way 1: set in the constructor (recommended)
mon = threading.Thread(target=background_monitor, daemon=True)

# Way 2: set the attribute BEFORE start()
# mon = threading.Thread(target=background_monitor)
# mon.daemon = True

mon.start()
time.sleep(3.5)
print("main exiting - daemon monitor dies automatically")
OUTPUT
[monitor] heartbeat [monitor] heartbeat [monitor] heartbeat main exiting - daemon monitor dies automatically
⚠️
Set daemon BEFORE start()daemon को start() से पहले set कीजिए在 start() 之前设置 daemon

You cannot change t.daemon once the thread is running. Set it in the constructor (Thread(..., daemon=True)) or right after Thread(...), before t.start(). Real run of the mistake:

Thread चलने के बाद t.daemon बदल नहीं सकते। इसे constructor में (Thread(..., daemon=True)) या Thread(...) के ठीक बाद, t.start() से पहले set कीजिए। ग़लती का असली run:

线程运行后就不能再修改 t.daemon。要在构造时设置(Thread(..., daemon=True)),或在 Thread(...) 之后、t.start() 之前设置。这个错误的真实运行:

import threading, time

t = threading.Thread(target=time.sleep, args=(1,))
t.start()
try:
    t.daemon = True              # too late!
except RuntimeError as e:
    print("RuntimeError:", e)
t.join()
OUTPUT
RuntimeError: cannot set daemon status of active thread

Classic example — the daemon is stopped before it finishesClassic example — daemon ख़त्म होने से पहले रोका गया经典示例 — 守护线程在完成前被停止

One non-daemon thread sleeps 3 seconds; one daemon thread sleeps 4 seconds. The program ends as soon as the non-daemon finishes, so the daemon never prints its exit message:

एक non-daemon thread 3 seconds sleep करता है; एक daemon thread 4 seconds। Non-daemon के ख़त्म होते ही program ख़त्म हो जाता है, इसलिए daemon अपना exit message कभी print नहीं करता:

一个非守护线程 sleep 3 秒,一个守护线程 sleep 4 秒。非守护线程一结束,程序就结束了,所以守护线程永远打印不出它的退出消息:

from threading import Thread
import time

def non_d():
    print("Non daemon enters")
    time.sleep(3)                # will finish at t = 3 s
    print("nonDaemon exits")

def d():
    print("daemon enters")
    time.sleep(4)                # would finish at t = 4 s... but is killed at 3 s
    print("Daemon exits")        # NEVER prints

nd = Thread(target=non_d)
nd.start()
d1 = Thread(target=d)
d1.setDaemon(True)               # old-style setter (deprecated)
d1.start()
OUTPUT
Non daemon enters demo.py:17: DeprecationWarning: setDaemon() is deprecated, set the daemon attribute instead d1.setDaemon(True) # old-style setter (deprecated) daemon enters nonDaemon exits
🔧
Fixed: setDaemon() Prints a Warningसुधार: setDaemon() एक Warning print करता है已修正:setDaemon() 会打印警告

The original said setDaemon(True) "still works" and showed clean output. It does still work, but since Python 3.10 it is deprecated, and because the call is in your main script Python shows a DeprecationWarning — the real output above includes it. Use daemon=True in the constructor or t.daemon = True.

मूल में कहा गया कि setDaemon(True) "अब भी चलता है" और साफ़ output दिखाया गया। यह चलता तो है, पर Python 3.10 से deprecated है, और call आपकी main script में होने से Python DeprecationWarning दिखाता है — ऊपर के असली output में यह है। Constructor में daemon=True या t.daemon = True लीजिए।

原文说 setDaemon(True)"仍然可用"并展示了干净的输出。它确实还能用,但从 Python 3.10 起已被弃用,而且由于调用写在主脚本里,Python 会显示一条 DeprecationWarning — 上面的真实输出中就有。请在构造时用 daemon=True,或写 t.daemon = True。

👻 Simulator: Daemon Timeline👻 Simulator: Daemon Timeline👻 模拟器:守护线程时间线

Choose how long the non-daemon and the daemon sleep. The program exits when the non-daemon finishes; a daemon still sleeping at that moment is cut off. All 16 combinations were run for real (at 1/4 speed).

चुनिए non-daemon और daemon कितना sleep करें। Non-daemon ख़त्म होने पर program exit करता है; उस पल sleep कर रहा daemon बीच में कट जाता है। सभी 16 combinations असल में चलाए गए (1/4 गति पर)।

选择非守护线程和守护线程各 sleep 多久。非守护线程结束时程序退出;那时仍在 sleep 的守护线程会被中途切断。全部 16 种组合都真实运行过(以 1/4 速度)。

Daemon TimelineDaemon Timeline守护线程时间线16 real runs16 असली runs16 次真实运行
non_d sleeps:non_d sleep:non_d 睡眠:
daemon sleeps:daemon sleep:守护线程睡眠:
OutputOutput输出

Try this variation — reverse the timesयह बदलाव आज़माइए — समय उलटिए试试这个变化 — 把时间反过来

from threading import Thread
import time

def non_d():
    print("Non daemon enters")
    time.sleep(4)                # now the LONGER one
    print("nonDaemon exits")

def d():
    print("daemon enters")
    time.sleep(3)                # now the SHORTER one
    print("Daemon exits")        # now it DOES print

nd = Thread(target=non_d); nd.start()
d1 = Thread(target=d); d1.daemon = True; d1.start()
OUTPUT
Non daemon enters daemon enters Daemon exits nonDaemon exits
🏆
The Full Pictureपूरी तस्वीर完整图景

Python keeps the program running as long as any non-daemon thread is still running. When the last one finishes, the program exits and every daemon thread is stopped wherever it is — mid-sleep, mid-loop, mid-write.

जब तक कोई भी non-daemon thread चल रहा है, Python program चलाता रहता है। आख़िरी के ख़त्म होते ही program exit करता है और हर daemon thread जहाँ है वहीं रोक दिया जाता है — sleep, loop या write के बीच में।

只要还有 任何 非守护线程在运行,Python 就会让程序继续运行。最后一个结束时,程序退出,所有守护线程无论在做什么都会被就地停止 — 无论是在 sleep、循环还是写入中途。

The danger of daemon threads — no clean shutdownDaemon threads का ख़तरा — साफ़ shutdown नहीं守护线程的危险 — 没有干净的关闭

import threading, time

def important_writer():
    try:
        with open("data.txt", "w") as f:
            for i in range(100):
                f.write(f"line {i}\n")
                time.sleep(0.05)          # pretend slow disk
        print("wrote all 100 lines")
    finally:
        print("cleanup ran")              # never printed for a killed daemon

# Marked as daemon -> main exits mid-write
t = threading.Thread(target=important_writer, daemon=True)
t.start()
time.sleep(1)                             # about 20 lines written so far
print("main done - daemon writer gets KILLED mid-write")
OUTPUT
main done - daemon writer gets KILLED mid-write $ wc -l data.txt 0 data.txt
🔧
Fixed: The File Was Empty, Not "About 20 Lines"सुधार: File ख़ाली थी, "लगभग 20 Lines" नहीं已修正:文件是空的,而不是"大约 20 行"

The original said data.txt would hold about 20 lines. Our real run left it with 0 lines. The 20 lines were written into Python's file buffer in memory; the buffer is only flushed to disk when it fills up or the file is closed — and a killed daemon never reaches the end of its with block. The finally block never ran either ("cleanup ran" is missing). So the real danger is even bigger: you can lose everything, not just the end.

मूल में कहा गया कि data.txt में लगभग 20 lines होंगी। हमारे असली run में यह 0 lines के साथ रही। 20 lines memory में Python के file buffer में लिखी गई थीं; buffer disk पर तभी flush होता है जब वह भरे या file बंद हो — और मारा गया daemon अपने with block के अंत तक कभी नहीं पहुँचता। finally block भी नहीं चला ("cleanup ran" ग़ायब है)। इसलिए असली ख़तरा और बड़ा है: आप सिर्फ़ अंत नहीं, सब कुछ खो सकते हैं।

原文说 data.txt 中大约会有 20 行。我们真实运行后,它是 0 行。那 20 行写进了 Python 在内存中的文件 缓冲区;缓冲区只有在写满或文件关闭时才会刷到磁盘 — 而被杀掉的守护线程永远走不到 with 块的结尾。finally 块也没有运行(没有出现"cleanup ran")。所以真正的危险更大:你可能丢失 全部 数据,而不只是结尾部分。

🔥
Never Make Threads That Own Resources DaemonsResources रखने वाले Threads को कभी Daemon मत बनाइए拥有资源的线程绝不要设为守护线程

At exit, daemon threads are simply abandoned: no finally blocks, no with clean-up, no buffer flushes. Never use a daemon for anything that writes files, holds a database transaction or must clean up. Use daemons only for background loops that keep nothing important.

Exit पर daemon threads बस छोड़ दिए जाते हैं: न finally blocks, न with की सफ़ाई, न buffer flush। Files लिखने, database transaction रखने या सफ़ाई करने वाले किसी भी काम के लिए daemon कभी मत लीजिए। Daemons सिर्फ़ उन background loops के लिए लीजिए जो कुछ ज़रूरी नहीं रखते।

程序退出时,守护线程会被直接抛弃:不运行 finally 块,不做 with 清理,不刷新缓冲区。凡是写文件、持有数据库事务或必须清理的工作,绝不要用守护线程。守护线程只适合不保存任何重要数据的后台循环。

🔊
Good for DaemonDaemon के लिए सही适合守护线程
background helpersbackground सहायक后台助手
Health-check tickers, cache-expiry loops, metrics collectors, watchdog timers. Anything that runs forever and owns nothing critical.
Health-check tickers, cache-expiry loops, metrics collectors, watchdog timers। जो हमेशा चलता है और कुछ ज़रूरी नहीं रखता।
健康检查定时器、缓存过期循环、指标收集器、看门狗定时器。任何一直运行且不持有关键数据的东西。
💾
Never Daemonकभी Daemon नहीं绝不设为守护
critical workज़रूरी काम关键工作
Writing files, database transactions, replies to real users, clean-up work — anything that must complete. Use a non-daemon thread and join().
Files लिखना, database transactions, असली users को जवाब, सफ़ाई का काम — जो पूरा होना ही चाहिए। Non-daemon thread और join() लीजिए।
写文件、数据库事务、给真实用户的响应、清理工作 — 任何必须完成的事。请用非守护线程加 join()。
⚖️
Clean Stopसाफ़ रुकना干净地停止
a stop signal + joinstop signal + join停止信号 + join
For a long-running non-daemon thread, give it a "please stop" flag and then join() it, so it can finish cleanly. The threading.Event tool for this is covered in an upcoming tutorial.
लंबे चलने वाले non-daemon thread को "कृपया रुको" flag दीजिए और फिर उसे join() कीजिए, ताकि वह साफ़ ख़त्म हो। इसका औज़ार threading.Event एक आने वाले tutorial में है।
对长期运行的非守护线程,给它一个"请停止"的标志,然后 join() 它,让它干净地结束。用于此的工具 threading.Event 会在 后续教程 中介绍。

SectionSection第 03 节

The Race Condition Problem 🏁Race Condition की समस्या 🏁竞态条件问题 🏁

Before locks, let us see the problem they solve. A race condition happens when two or more threads read-modify-write the same value and the way their steps interleave produces a wrong result.

Locks से पहले, वह समस्या देखते हैं जिसे वे हल करते हैं। Race condition तब होती है जब दो या ज़्यादा threads एक ही value को read-modify-write करते हैं और उनके क़दमों के आपस में मिलने से ग़लत नतीजा आता है।

在介绍锁之前,先看看它们要解决的问题。当两个或更多线程对同一个值进行"读-改-写",而它们的步骤交错执行导致结果错误时,就发生了 竞态条件。

Figure 2 — How counter += 1 Loses Updatesचित्र 2 — counter += 1 Updates कैसे खोता है图 2 — counter += 1 是如何丢失更新的
How counter += 1 loses updatescounter += 1 is really THREE steps: read → add 1 → write backcounter += 1 असल में तीन क़दम है: read → 1 जोड़ो → वापस writecounter += 1 实际上是三步:读取 → 加 1 → 写回THREAD ATHREAD A线程 ATHREAD BTHREAD B线程 B1) read → 1001) read → 100A hasn't written yet!A ने अभी लिखा नहीं!A 还没写回!2) add 1 → 1012) add 1 → 1013) write 1013) write 101overwrites A!A के ऊपर लिखा!覆盖了 A 的结果!SHARED counterSHARED counter共享的 counter100 → 101should be 102!

Python can switch threads between the read and the write. Both threads see the old value, so one update is lost.

Python read और write के बीच threads बदल सकता है। दोनों threads पुरानी value देखते हैं, इसलिए एक update खो जाता है।

Python 可能在读取和写回之间切换线程。两个线程都看到旧值,于是丢失了一次更新。

Race condition demo — 10 threads × 100,000 incrementsRace condition demo — 10 threads × 100,000 increments竞态条件演示 — 10 个线程 × 100,000 次自增

import threading

counter = 0

def increment():
    global counter
    for _ in range(100_000):
        counter += 1              # NOT atomic!

threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()

print(f"expected: {10 * 100_000}")
print(f"got:      {counter}")
print(f"lost:     {10 * 100_000 - counter} increments")
OUTPUT
expected: 1000000 got: 1000000 lost: 0 increments
🔧
Fixed: Our Run Lost Nothing — and That Is Not Safeसुधार: हमारे Run में कुछ नहीं खोया — और यह सुरक्षित नहीं है已修正:我们的运行一个也没丢 — 但这并不安全

The original showed a typical result like "got 783412, lost 216588". On Python 3.13 we ran this exact code five times and got exactly 1,000,000 every time. Newer CPython versions switch threads at fewer places, so this tiny loop rarely shows the bug. But counter += 1 is still not guaranteed to be atomic — the same code can lose updates on another Python version, another machine, a free-threaded build, or when the work between read and write takes longer. The next example widens that gap on purpose, and the bug appears at once.

मूल में "got 783412, lost 216588" जैसा आम नतीजा दिखाया गया था। Python 3.13 पर हमने यही code पाँच बार चलाया और हर बार ठीक 1,000,000 मिला। नए CPython versions कम जगहों पर threads बदलते हैं, इसलिए यह छोटा loop bug शायद ही दिखाता है। पर counter += 1 की atomic होने की गारंटी अब भी नहीं — वही code दूसरे Python version, दूसरी machine, free-threaded build पर, या read और write के बीच का काम लंबा होने पर updates खो सकता है। अगला example जान-बूझकर यह gap बढ़ाता है, और bug तुरंत दिखता है।

原文展示了类似"got 783412, lost 216588"的典型结果。我们在 Python 3.13 上把这段代码原样运行了五次,每次都恰好是 1,000,000。较新的 CPython 版本切换线程的位置更少,所以这个小循环很少暴露这个 bug。但 counter += 1 仍然 不保证 是原子操作 — 同样的代码在其他 Python 版本、其他机器、自由线程版本上,或在读取和写回之间的工作更耗时时,都可能丢失更新。下一个例子故意把这个间隙拉大,bug 立刻就出现了。

import threading, time

counter = 0

def increment():
    global counter
    for _ in range(1_000):
        value = counter           # 1) read
        time.sleep(0)             #    let another thread run here
        counter = value + 1       # 2) add 1  3) write back

threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()

print(f"expected: {10 * 1_000}")
print(f"got:      {counter}")
print(f"lost:     {10 * 1_000 - counter} increments")
OUTPUT
expected: 10000 got: 1002 lost: 8998 increments

👣 Simulator: Race Stepper👣 Simulator: Race Stepper👣 模拟器:竞态单步演示

You are the scheduler. Each thread wants to add 1 to counter twice, in three steps (read, add, write). Click which thread runs the next step and try to lose an update. Then turn the lock on: a thread that tries to acquire() while the other holds the lock is blocked.

आप scheduler हैं। हर thread counter में दो बार 1 जोड़ना चाहता है, तीन क़दमों में (read, add, write)। Click करके चुनिए अगला क़दम कौन-सा thread चलाए और एक update खोने की कोशिश कीजिए। फिर lock चालू कीजिए: दूसरे के lock पकड़े रहते acquire() करने वाला thread रुक जाता है।

你就是调度器。每个线程想给 counter 加两次 1,每次分三步(读、加、写)。点击选择由哪个线程执行下一步,试着让一次更新丢失。然后打开锁:当另一个线程持有锁时,尝试 acquire() 的线程会被阻塞。

Race StepperRace Stepper竞态单步演示you are the schedulerआप scheduler हैं你来当调度器
Lock:Lock:锁:
shared counterसाझा counter共享 counter
lock holderlock किसके पास锁的持有者
Thread AThread A线程 A
Thread BThread B线程 B

📊 Simulator: Race Lab — real counts📊 Simulator: Race Lab — असली गिनती📊 模拟器:竞态实验室 — 真实计数

Each program was run five times on Python 3.13. Green bars hit the expected total; red bars lost updates.

हर program Python 3.13 पर पाँच बार चलाया गया। हरी bars अपेक्षित कुल तक पहुँचीं; लाल bars ने updates खोए।

每个程序都在 Python 3.13 上运行了五次。绿色条达到了预期总数;红色条丢失了更新。

Race LabRace Lab竞态实验室15 real runs15 असली runs15 次真实运行
counter += 1 happened to survive on this Python. Add a tiny gap between read and write and about 90% of the updates are lost. With a lock, every run is exact.इस Python पर counter += 1 संयोग से बच गया। Read और write के बीच छोटा-सा gap जोड़िए और लगभग 90% updates खो जाते हैं। Lock के साथ हर run सटीक है।在这个 Python 版本上 counter += 1 碰巧没出问题。在读取和写回之间加一个小小的间隙,大约 90% 的更新就丢失了。加锁后,每次运行都完全正确。
🔥
Silent Data Corruptionचुपचाप बिगड़ता Data悄无声息的数据损坏

No exception. No warning. Just wrong numbers. This is the most dangerous kind of bug: it does not crash, and because thread timing changes from run to run, it can look fine in testing and fail only under real load.

कोई exception नहीं। कोई warning नहीं। बस ग़लत numbers। यह सबसे ख़तरनाक तरह का bug है: यह crash नहीं होता, और क्योंकि thread timing हर run में बदलती है, यह testing में ठीक दिख सकता है और सिर्फ़ असली load में fail हो सकता है।

没有异常,没有警告,只有错误的数字。这是最危险的一类 bug:它不会崩溃,而且由于线程的时序每次运行都不同,它可能在测试中看起来没问题,只在真实负载下才出错。


SectionSection第 04 节

Lock — The Turn-Taking Token 🔒Lock — बारी वाला Token 🔒Lock — 轮流令牌 🔒

threading.Lock is Python's most basic synchronization tool. It has two states — unlocked and locked — and two operations — acquire() and release(). Only one thread can hold the lock at a time. Any other thread that calls acquire() sleeps until the holder calls release().

threading.Lock Python का सबसे बुनियादी synchronization औज़ार है। इसकी दो states हैं — unlocked और locked — और दो operations — acquire() और release()। एक समय पर सिर्फ़ एक thread lock रख सकता है। acquire() बुलाने वाला कोई और thread तब तक सोता है जब तक रखने वाला release() न बुलाए।

threading.Lock 是 Python 最基本的同步工具。它有两种状态 — 未锁定 和 已锁定 — 以及两个操作 — acquire() 和 release()。同一时刻只有一个线程能持有锁。其他调用 acquire() 的线程会休眠,直到持有者调用 release()。

Figure 3 — How a Lock Serialises Accessचित्र 3 — Lock Access को कतार में कैसे लगाता है图 3 — 锁如何让访问串行化
How a lock serialises accessThread AThread BThread C🔒 lockCRITICAL SECTIONCRITICAL SECTION临界区one thread at a timeएक समय में एक thread一次只允许一个线程inside ✓अंदर ✓进入 ✓waits…इंतज़ार…等待…waits…इंतज़ार…等待…SHARED DATASHARED DATA共享数据counterrelease() → the next waiting thread wakes up and goes inrelease() → अगला इंतज़ार करता thread जागकर अंदर जाता हैrelease() → 下一个等待的线程被唤醒并进入

The lock does not protect the data directly — it protects the critical section: the code that touches the data. Every thread must agree to acquire it first.

Lock data को सीधे नहीं बचाता — यह critical section बचाता है: वह code जो data छूता है। हर thread को पहले इसे acquire करने पर सहमत होना होगा।

锁并不直接保护数据 — 它保护的是 临界区:也就是访问数据的那段代码。每个线程都必须先同意获取它。

Fixing the race conditionRace condition ठीक करना修复竞态条件

Here is the "gap" program from Section 03, which lost about 90% of its updates. We add a lock around the read-modify-write and change nothing else:

यह Section 03 वाला "gap" program है, जिसने लगभग 90% updates खोए थे। हम read-modify-write के चारों ओर lock लगाते हैं और कुछ और नहीं बदलते:

这是第 03 节中丢失了约 90% 更新的"间隙"程序。我们在读-改-写周围加一把锁,其他什么都不改:

import threading, time

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(1_000):
        lock.acquire()            # wait if someone else holds it
        value = counter
        time.sleep(0)             # the same "risky" gap as before
        counter = value + 1       # now safe - we're alone
        lock.release()            # let the next thread in

threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()

print(f"expected: {10 * 1_000}")
print(f"got:      {counter}")
OUTPUT
expected: 10000 got: 10000
✅
Exactly 10,000 — Every Timeहर बार ठीक 10,000每次都恰好 10,000

The time.sleep(0) still lets other threads run in the middle — but now they are all waiting at lock.acquire(), so none of them can read counter until the holder has written it back.

time.sleep(0) अब भी बीच में दूसरे threads को चलने देता है — पर अब वे सब lock.acquire() पर इंतज़ार कर रहे हैं, इसलिए जब तक रखने वाला वापस न लिख दे, कोई counter नहीं पढ़ सकता।

time.sleep(0) 仍然会让其他线程在中途运行 — 但现在它们都在 lock.acquire() 处等待,所以在持有者写回之前,没有线程能读取 counter。

The right way — the with statementसही तरीक़ा — with statement正确做法 — with 语句

Calling acquire() and release() by hand is fragile. If the code between them raises an exception, release() never runs, the lock stays locked for ever, and every other thread waits for ever. The with statement calls acquire() on entry and guarantees release() on exit — even on an exception.

acquire() और release() हाथ से बुलाना नाज़ुक है। अगर उनके बीच का code exception raise करे, तो release() कभी नहीं चलता, lock हमेशा के लिए locked रहता है, और बाक़ी हर thread हमेशा इंतज़ार करता है। with statement अंदर जाते समय acquire() बुलाता है और बाहर निकलते समय release() की गारंटी देता है — exception पर भी।

手动调用 acquire() 和 release() 很脆弱。如果两者之间的代码抛出异常,release() 就永远不会运行,锁会一直保持锁定,其他所有线程都会永远等待。with 语句在进入时调用 acquire(),并在退出时 保证 调用 release() — 即使发生异常也一样。

❌ Fragile — manual releaseनाज़ुक — हाथ से release脆弱 — 手动释放
lock.acquire()
risky_operation()   # may raise
lock.release()      # never runs on an exception!
The lock leaks → other threads wait for everLock leak → बाक़ी threads हमेशा इंतज़ार锁泄漏 → 其他线程永远等待
✅ Safe — with blockसुरक्षित — with block安全 — with 块
with lock:
    risky_operation()
Released even if an exception is raisedException raise होने पर भी release即使抛出异常也会释放
You can never forget to releaseRelease भूलना असंभव永远不会忘记释放
import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100_000):
        with lock:                # acquires on enter, releases on exit
            counter += 1          # exception-safe

threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(f"counter = {counter}")
OUTPUT
counter = 1000000
🩺
See It in the Bug DoctorBug Doctor में देखिए在 Bug 医生中查看

The first case in the Bug Doctor (Section 09) runs the fragile version for real: after the exception is handled, lock.acquire(timeout=1) returns False — the lock leaked. With with it returns True.

Bug Doctor (Section 09) का पहला case नाज़ुक version सच में चलाता है: exception handle होने के बाद lock.acquire(timeout=1) False लौटाता है — lock leak हो गया। with के साथ यह True लौटाता है।

Bug 医生(第 09 节)的第一个案例真实运行了脆弱版本:异常被处理后,lock.acquire(timeout=1) 返回 False — 锁泄漏了。使用 with 时它返回 True。

Lock methods referenceLock methods referenceLock 方法速查

MethodMethod方法What it doesक्या करता है作用Common useआम इस्तेमाल常见用途
lock.acquire()Waits until the lock is free, then takes it. Returns True.Lock ख़ाली होने तक इंतज़ार, फिर ले लेता है। True लौटाता है।等到锁空闲后获取它,返回 True。Most commonसबसे आम最常用
lock.acquire(blocking=False)Tries once; returns False at once if the lock is takenएक बार कोशिश; lock लिया हुआ हो तो तुरंत False只尝试一次;如果锁已被占用则立即返回 FalseSkip work if busyव्यस्त हो तो काम छोड़ें忙时跳过工作
lock.acquire(timeout=2)Waits up to 2 s; returns False on timeout2 s तक इंतज़ार; timeout पर False最多等待 2 秒;超时返回 FalseAvoid hanging for everहमेशा अटकने से बचें避免永久挂起
lock.release()Frees the lock. RuntimeError if it is not locked.Lock मुक्त करता है। Locked न हो तो RuntimeError।释放锁。如果锁未锁定则抛出 RuntimeError。Prefer withwith बेहतर优先用 with
lock.locked()True if some thread holds the lock right nowअभी कोई thread lock रखे हो तो True如果此刻有线程持有锁则为 TrueDiagnostics onlyसिर्फ़ जाँच के लिए仅用于诊断
with lock:Acquire on entry, release on exit — even on an exceptionअंदर जाते acquire, निकलते release — exception पर भी进入时获取,退出时释放 — 异常时也一样Always prefer thisहमेशा यही始终优先使用
⚠️
locked() Is Only a Snapshotlocked() सिर्फ़ एक झलक हैlocked() 只是一个快照

Never write if not lock.locked(): lock.acquire() — another thread can grab the lock between the check and the acquire. If you want "take it only if free", use lock.acquire(blocking=False), which checks and takes in one step.

if not lock.locked(): lock.acquire() कभी मत लिखिए — check और acquire के बीच कोई दूसरा thread lock ले सकता है। "ख़ाली हो तभी लो" चाहिए तो lock.acquire(blocking=False) लीजिए, जो एक ही क़दम में जाँचता और लेता है।

绝不要写 if not lock.locked(): lock.acquire() — 在检查和获取之间,另一个线程可能抢走锁。如果你想"空闲时才获取",请用 lock.acquire(blocking=False),它在一步中完成检查和获取。

Non-blocking acquire — skip instead of waitNon-blocking acquire — इंतज़ार की जगह छोड़ दें非阻塞获取 — 跳过而不是等待

import threading, time

lock = threading.Lock()

def try_get_lock(worker_id):
    if lock.acquire(blocking=False):
        try:
            print(f"worker {worker_id} got the lock, working...")
            time.sleep(1)
        finally:
            lock.release()
    else:
        print(f"worker {worker_id} could not get lock - skipping")

threads = [threading.Thread(target=try_get_lock, args=(i,)) for i in range(4)]
for t in threads: t.start()
for t in threads: t.join()
OUTPUT (which worker wins can vary)OUTPUT (कौन-सा worker जीते, बदल सकता है)输出(哪个 worker 抢到可能不同)
worker 0 got the lock, working... worker 1 could not get lock - skipping worker 2 could not get lock - skipping worker 3 could not get lock - skipping

Worker 0 took the lock and held it for 1 second. Workers 1–3 tried a split second later, got False at once, and moved on instead of waiting. Note the try/finally: once you acquire by hand, you must release in finally.

Worker 0 ने lock लिया और 1 second रखा। Workers 1–3 ने पल भर बाद कोशिश की, तुरंत False पाया, और इंतज़ार करने की जगह आगे बढ़ गए। try/finally देखिए: हाथ से acquire करने पर release finally में ही करना चाहिए।

Worker 0 拿到锁并持有 1 秒。Worker 1–3 稍后一瞬间尝试,立刻得到 False,于是继续往下走而不是等待。注意 try/finally:一旦手动获取,就必须在 finally 中释放。


SectionSection第 05 节

Deadlock — When Locks Attack 💀Deadlock — जब Locks ही फँसा दें 💀死锁 — 当锁反过来咬人 💀

A deadlock happens when two or more threads each wait for a lock that another one holds. Nothing crashes and nothing prints — the program just freezes.

Deadlock तब होता है जब दो या ज़्यादा threads हर एक ऐसे lock का इंतज़ार करते हैं जो किसी दूसरे के पास है। कुछ crash नहीं होता, कुछ print नहीं होता — program बस जम जाता है।

当两个或更多线程各自等待另一个线程持有的锁时,就发生了 死锁。没有崩溃,也没有输出 — 程序就这么冻住了。

Figure 4 — The Classic Deadlock Cycleचित्र 4 — Classic Deadlock चक्र图 4 — 经典死锁环
The classic deadlock cycleThread Aholds lock1lock1 रखे है持有 lock1Thread Bholds lock2lock2 रखे है持有 lock2🔒 lock1🔒 lock2owned byइसके पास归属owned byइसके पास归属waits forइंतज़ार等待waits forइंतज़ार等待∞a cycle — nobody can moveएक चक्र — कोई आगे नहीं बढ़ सकता形成环 — 谁也动不了

Each thread holds one lock and needs the other. Neither can go on. To the operating system they are just "waiting", not "crashed" — so the program hangs silently.

हर thread एक lock रखे है और दूसरा चाहता है। कोई आगे नहीं जा सकता। Operating system के लिए वे बस "इंतज़ार" में हैं, "crash" नहीं — इसलिए program चुपचाप अटका रहता है।

每个线程持有一把锁并需要另一把。谁都无法继续。在操作系统看来,它们只是在"等待"而不是"崩溃" — 所以程序会悄无声息地挂起。

import threading, time

lock1 = threading.Lock()
lock2 = threading.Lock()

def thread_a():
    with lock1:
        print("A got lock1")
        time.sleep(0.1)           # give B time to grab lock2
        with lock2:               # will wait FOREVER
            print("A got lock2 (never prints)")

def thread_b():
    with lock2:
        print("B got lock2")
        time.sleep(0.1)
        with lock1:               # will wait FOREVER
            print("B got lock1 (never prints)")

threading.Thread(target=thread_a).start()
threading.Thread(target=thread_b).start()
# Program hangs - Ctrl-C to exit
OUTPUT
A got lock1 B got lock2 [we stopped the program after 2 s — it would hang for ever]

A grabs lock1 and B grabs lock2. The short sleep makes sure both happen before either asks for its second lock. Then A waits for lock2 (held by B) and B waits for lock1 (held by A). Our run printed two lines and then sat still until we killed it.

A lock1 लेता है और B lock2। छोटा sleep पक्का करता है कि दोनों अपना दूसरा lock माँगने से पहले हो जाएँ। फिर A lock2 (B के पास) का इंतज़ार करता है और B lock1 (A के पास) का। हमारे run ने दो lines print कीं और फिर तब तक रुका रहा जब तक हमने उसे बंद नहीं किया।

A 拿到 lock1,B 拿到 lock2。短暂的 sleep 确保两者都在请求第二把锁之前完成。接着 A 等待 lock2(被 B 持有),B 等待 lock1(被 A 持有)。我们的运行打印了两行,然后一直静止,直到我们把它杀掉。

💀 Simulator: Deadlock Lab💀 Simulator: Deadlock Lab💀 模拟器:死锁实验室

Three versions of the two-lock program, each run for real (events in seconds). Watch the lock graph: a green line means "holds", a red dashed arrow means "waits for". A red cycle is a deadlock.

दो-lock program के तीन versions, हर एक असल में चलाया गया (events seconds में)। Lock graph देखिए: हरी line मतलब "रखे है", लाल dashed तीर मतलब "इंतज़ार कर रहा है"। लाल चक्र = deadlock।

双锁程序的三个版本,每个都真实运行过(事件以秒为单位)。观察锁关系图:绿线表示"持有",红色虚线箭头表示"等待"。红色的环就是死锁。

Deadlock LabDeadlock Lab死锁实验室3 real runs3 असली runs3 次真实运行
Version:Version:版本:
CodeCode代码
Events (real run)Events (असली run)事件(真实运行)
🔑
The Lock-Ordering RuleLock-Ordering नियम加锁顺序规则

To prevent deadlocks, every thread must acquire multiple locks in the same global order. If everyone takes lock1 before lock2 (never the reverse), the cycle in Figure 4 cannot form — that is the "same order" run above. A second defence is lock.acquire(timeout=N): a thread that cannot get the second lock gives up, releases what it holds, waits a moment and tries again — the "timeout" run.

Deadlocks रोकने के लिए हर thread को कई locks एक ही global क्रम में acquire करने चाहिए। अगर सब lock1 को lock2 से पहले लें (कभी उलटा नहीं), तो चित्र 4 का चक्र बन ही नहीं सकता — ऊपर का "एक ही क्रम" run यही है। दूसरा बचाव lock.acquire(timeout=N) है: जो thread दूसरा lock नहीं पा सकता वह हार मानता है, जो रखे है उसे छोड़ता है, थोड़ा रुकता है और फिर कोशिश करता है — "timeout" run।

为防止死锁,每个线程都必须以 相同的全局顺序 获取多把锁。如果所有线程都先拿 lock1 再拿 lock2(绝不反过来),图 4 中的环就无法形成 — 这就是上面的"相同顺序"运行。第二道防线是 lock.acquire(timeout=N):拿不到第二把锁的线程会放弃,释放已持有的锁,稍等片刻再重试 — 也就是"超时"运行。


SectionSection第 06 节

RLock — The Reentrant Lock 🔁RLock — Reentrant Lock 🔁RLock — 可重入锁 🔁

Sometimes one thread needs to acquire the same lock more than once — usually because one locked function calls another locked function. With a plain Lock the thread blocks against itself. RLock (reentrant lock) fixes this: it remembers which thread owns it and how many times that thread has acquired it.

कभी-कभी एक thread को उसी lock को एक से ज़्यादा बार acquire करना पड़ता है — आम तौर पर क्योंकि एक locked function दूसरे locked function को बुलाता है। साधारण Lock के साथ thread ख़ुद से ही अटक जाता है। RLock (reentrant lock) इसे ठीक करता है: यह याद रखता है कि कौन-सा thread इसका मालिक है और उसने इसे कितनी बार acquire किया है।

有时一个线程需要多次获取 同一把 锁 — 通常是因为一个加锁的函数调用了另一个加锁的函数。用普通的 Lock,线程会被自己阻塞。RLock(可重入锁)解决了这个问题:它记录 哪个线程拥有它,以及该线程 获取了多少次。

Figure 5 — Lock vs RLock When the Same Thread Re-acquiresचित्र 5 — वही Thread दोबारा Acquire करे तो Lock बनाम RLock图 5 — 同一线程再次获取时的 Lock 与 RLock
Lock vs RLock when the same thread re-acquiresPLAIN Lockसाधारण Lock普通 LockRLockacquire() #1 → ✓ lockedacquire() #1 → owner=T, count 1acquire() #2 → waits…it does not know who holds itइसे नहीं पता कौन रखे है它不知道是谁持有它acquire() #2 → same owner, count 2⏳ waiting for ITSELFself-deadlock — never endsself-deadlock — कभी ख़त्म नहीं自我死锁 — 永不结束release() → count 1 (still held)release() → count 0 → FREE✓ other threads may enter

A plain Lock does not track an owner, so a second acquire from the same thread simply waits — for ever. An RLock tracks the owner and a counter, so the same thread may acquire again; the lock is freed only when the counter is back to 0.

साधारण Lock owner नहीं रखता, इसलिए उसी thread का दूसरा acquire बस इंतज़ार करता है — हमेशा। RLock owner और counter रखता है, इसलिए वही thread फिर acquire कर सकता है; counter वापस 0 होने पर ही lock मुक्त होता है।

普通 Lock 不记录拥有者,所以同一线程的第二次获取只会一直等待 — 永远。RLock 记录拥有者和计数器,所以同一线程可以再次获取;只有计数器回到 0 时锁才会被释放。

The problem a plain Lock createsसाधारण Lock की पैदा की हुई समस्या普通 Lock 带来的问题

import threading

lock = threading.Lock()

def deposit(amount):
    with lock:                        # acquire #1
        print(f"deposit {amount}")
        audit(f"deposit of {amount}") # calls audit - which also locks!

def audit(msg):
    with lock:                        # acquire #2 - SELF-DEADLOCK
        print(f"[audit] {msg}")

t = threading.Thread(target=deposit, args=(100,), daemon=True)
t.start()
t.join(timeout=2)
print(f"alive after 2s? {t.is_alive()}")    # True - deadlocked
OUTPUT
deposit 100 alive after 2s? True
🔧
Fixed: The Self-Deadlock Demo Would Never Exitसुधार: Self-Deadlock Demo कभी Exit नहीं होता已修正:自我死锁演示永远不会退出

The original started this thread as a normal (non-daemon) thread. After printing alive after 2s? True, Python would wait for that stuck thread at exit — so the program would hang for ever. We made the demo thread a daemon (daemon=True), which is exactly the right use: it lets the program exit even though the thread is stuck.

मूल में यह thread साधारण (non-daemon) thread के रूप में शुरू किया गया था। alive after 2s? True print करने के बाद Python exit पर उस अटके thread का इंतज़ार करता — यानी program हमेशा अटका रहता। हमने demo thread को daemon (daemon=True) बनाया, जो बिल्कुल सही इस्तेमाल है: thread अटका होने पर भी program exit कर पाता है।

原文把这个线程作为普通(非守护)线程启动。打印 alive after 2s? True 之后,Python 会在退出时等待那个卡住的线程 — 所以程序会永远挂起。我们把演示线程设为守护线程(daemon=True),这正是它的正确用法:即使线程卡住,程序也能退出。

The fix — use RLockFix — RLock लीजिए修复 — 使用 RLock

import threading

rlock = threading.RLock()             # reentrant version

def deposit(amount):
    with rlock:                       # count = 1
        print(f"deposit {amount}")
        audit(f"deposit of {amount}")

def audit(msg):
    with rlock:                       # count = 2 - same thread, OK
        print(f"[audit] {msg}")

t = threading.Thread(target=deposit, args=(100,))
t.start()
t.join()
print("finished cleanly")
OUTPUT
deposit 100 [audit] deposit of 100 finished cleanly

How the counter works — nested acquiresCounter कैसे चलता है — nested acquires计数器如何工作 — 嵌套获取

import threading

lock = threading.RLock()

def first(n):
    print("In first", n)
    lock.acquire()                # count: 1 -> 2  (same thread, allowed)
    a = 12 + n
    lock.release()                # count: 2 -> 1
    print(a)

def second(n):
    lock.acquire()                # count: 1 -> 2  (same thread, allowed)
    b = 12 + n
    lock.release()                # count: 2 -> 1
    print(b)

def all():
    print("start")
    lock.acquire()                # count: 0 -> 1  (thread owns it now)
    first(2)                      # re-acquires + releases inside
    second(3)                     # re-acquires + releases inside
    lock.release()                # count: 1 -> 0  (truly freed)

th1 = threading.Thread(target=all)
th1.start()
OUTPUT
start In first 2 14 15
🔁 Trace of the RLock counterRLock counter का traceRLock 计数器追踪
1
all() prints "start" and calls lock.acquire(). Nobody owns the lock, so this thread becomes the owner. Count 0 → 1.
all() "start" print करता है और lock.acquire() बुलाता है। Lock का कोई मालिक नहीं, इसलिए यह thread मालिक बन जाता है। Count 0 → 1।
all() 打印 "start" 并调用 lock.acquire()。锁没有拥有者,于是这个线程成为拥有者。计数 0 → 1。
2
first(2) prints "In first 2" and calls lock.acquire(). Same owner, so it is allowed. Count 1 → 2.
first(2) "In first 2" print करता है और lock.acquire() बुलाता है। वही मालिक, इसलिए इजाज़त है। Count 1 → 2।
first(2) 打印 "In first 2" 并调用 lock.acquire()。同一个拥有者,所以允许。计数 1 → 2。
3
a = 14, then lock.release(). Count 2 → 1 — the lock is still held. Prints 14.
a = 14, फिर lock.release()। Count 2 → 1 — lock अब भी पकड़ा हुआ है। 14 print।
a = 14,然后 lock.release()。计数 2 → 1 — 锁 仍被持有。打印 14。
4
second(3): acquire (1 → 2), b = 15, release (2 → 1). Prints 15.
second(3): acquire (1 → 2), b = 15, release (2 → 1)। 15 print।
second(3):获取(1 → 2),b = 15,释放(2 → 1)。打印 15。
5
Back in all(): lock.release(). Count 1 → 0. Now the lock is really free, and another thread could take it.
वापस all() में: lock.release()। Count 1 → 0। अब lock सच में मुक्त है, और कोई दूसरा thread इसे ले सकता है।
回到 all():lock.release()。计数 1 → 0。现在锁才真正空闲,其他线程可以获取它。
❌ With a plain Lock — stops at step 2साधारण Lock के साथ — step 2 पर रुकता है用普通 Lock — 停在第 2 步
all() acquires the lock ✓all() lock acquire करता है ✓all() 获取锁 ✓
first() prints "In first 2"first() "In first 2" print करता हैfirst() 打印 "In first 2"
first() calls acquire() → waits for itselffirst() acquire() बुलाता है → ख़ुद का इंतज़ारfirst() 调用 acquire() → 等待自己
Output stops after "In first 2"Output "In first 2" के बाद रुक जाता है输出停在 "In first 2" 之后
✅ With RLock — completes cleanlyRLock के साथ — साफ़ पूरा होता है用 RLock — 顺利完成
all() acquires (count 1)all() acquire (count 1)all() 获取(计数 1)
first(): count 2, then 1first(): count 2, फिर 1first():计数 2,然后 1
second(): count 2, then 1second(): count 2, फिर 1second():计数 2,然后 1
all() releases (count 0) — all output printedall() release (count 0) — पूरा output printall() 释放(计数 0)— 输出全部打印
🔑
Golden Take-Awayमुख्य सीख核心收获

Every acquire() by the owner adds 1 to the counter. Every release() subtracts 1. The lock is only really freed when the count reaches zero. That is why nested calls that each take the same lock work with RLock — as long as every acquire has a matching release.

मालिक का हर acquire() counter में 1 जोड़ता है। हर release() 1 घटाता है। Count शून्य होने पर ही lock सच में मुक्त होता है। इसीलिए एक ही lock लेने वाली nested calls RLock के साथ चलती हैं — बशर्ते हर acquire का एक मिलता release हो।

拥有者的每次 acquire() 让计数器加 1,每次 release() 减 1。只有计数归零时锁才真正释放。这就是为什么每层都获取同一把锁的嵌套调用在 RLock 下能正常工作 — 前提是每次获取都有对应的释放。

Problem #2 — cross-thread release (where RLock refuses)समस्या #2 — दूसरे thread से release (जहाँ RLock मना करता है)问题 #2 — 跨线程释放(RLock 拒绝的情况)

Here is a case that looks like RLock should save us — but it cannot, because RLock enforces ownership. One thread acquires and never releases; a different thread tries to release for it.

यह ऐसा case है जिसमें लगता है RLock बचा लेगा — पर नहीं बचा सकता, क्योंकि RLock मालिकाना हक़ लागू करता है। एक thread acquire करता है और कभी release नहीं करता; दूसरा thread उसकी जगह release करने की कोशिश करता है।

下面这个案例看起来 RLock 应该能救场 — 但它不能,因为 RLock 强制执行 所有权。一个线程获取后从不释放;另一个线程试图替它释放。

import threading
import time
import datetime

lock = threading.RLock()
t1 = datetime.datetime.now()

def second(n):
    lock.acquire()                # acquires but NEVER releases!
    print(n)

def third():
    time.sleep(5)
    lock.release()                # releases a lock this thread never acquired!
    print("Thread3")

th1 = threading.Thread(target=second, args=("Thread1",))
th1.start()
th2 = threading.Thread(target=second, args=("Thread2",))
th2.start()
th3 = threading.Thread(target=third)
th3.start()

th1.join()
th2.join()
th3.join()
t2 = datetime.datetime.now()
print("Total time", t2 - t1)
OUTPUT
Thread1 Exception in thread Thread-3 (third): Traceback (most recent call last): File "/usr/lib/python3.13/threading.py", line 1044, in _bootstrap_inner self.run() ~~~~~~~~^^ File "/usr/lib/python3.13/threading.py", line 995, in run self._target(*self._args, **self._kwargs) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "demo.py", line 14, in third lock.release() # releases a lock this thread never acquired! ~~~~~~~~~~~~^^ RuntimeError: cannot release un-acquired lock [we stopped the program after 7 s — it would hang for ever]
🚨 What actually happensअसल में क्या होता है实际发生了什么
t = 0 s
th1 runs second("Thread1"): acquires, prints "Thread1", and its function returns without releasing. th1 ends — but still owns the lock.
th1 second("Thread1") चलाता है: acquire करता है, "Thread1" print करता है, और function release किए बिना लौट आता है। th1 ख़त्म — पर lock का मालिक अब भी वही।
th1 运行 second("Thread1"):获取锁,打印 "Thread1",函数在 没有释放 的情况下返回。th1 结束了 — 但仍然拥有这把锁。
t = 0 s
th2 runs second("Thread2"): acquire() blocks — the lock belongs to th1.
th2 second("Thread2") चलाता है: acquire() रुक जाता है — lock th1 का है।
th2 运行 second("Thread2"):acquire() 被阻塞 — 锁属于 th1。
t = 5 s
th3 wakes and calls lock.release(), but th3 never acquired it → RuntimeError: cannot release un-acquired lock. th3 dies; "Thread3" is never printed.
th3 जागता है और lock.release() बुलाता है, पर th3 ने इसे कभी acquire नहीं किया → RuntimeError: cannot release un-acquired lock। th3 मर जाता है; "Thread3" कभी print नहीं होता।
th3 醒来并调用 lock.release(),但 th3 从未获取过它 → RuntimeError: cannot release un-acquired lock。th3 终止;"Thread3" 永远不会被打印。
t = ∞
th2 stays blocked, so th2.join() in main waits for ever and "Total time" never prints. (We stopped it after 7 s.)
th2 रुका रहता है, इसलिए main में th2.join() हमेशा इंतज़ार करता है और "Total time" कभी print नहीं होता। (हमने 7 s बाद रोका।)
th2 一直被阻塞,所以主线程中的 th2.join() 永远等待,"Total time" 永远不会打印。(我们在 7 秒后停止了它。)
🔥
Two Fatal Anti-Patterns in One Programएक Program में दो घातक Anti-Patterns一个程序中的两个致命反模式

(1) Never split acquire and release across different functions or threads — they belong in the same block, ideally a with block. (2) Only the owning thread may call release() on an RLock; any other thread gets RuntimeError.

(1) Acquire और release को कभी अलग functions या threads में मत बाँटिए — वे एक ही block में होने चाहिए, बेहतर हो with block में। (2) RLock पर release() सिर्फ़ मालिक thread बुला सकता है; कोई और thread RuntimeError पाता है।

(1) 绝不要把获取和释放拆到不同的函数或线程中 — 它们应该在同一个代码块里,最好是 with 块。(2) 只有拥有者线程才能对 RLock 调用 release();其他线程会得到 RuntimeError。

The fix — each function manages its own lockFix — हर function अपना lock ख़ुद संभाले修复 — 每个函数自己管理锁

import threading, time, datetime

lock = threading.RLock()
t1 = datetime.datetime.now()

def second(n):
    with lock:                    # acquire + guaranteed release
        print(n)
        time.sleep(1)             # pretend we're doing critical work

def third():
    time.sleep(2)
    with lock:                    # properly acquire before touching shared state
        print("Thread3")

threads = [
    threading.Thread(target=second, args=("Thread1",)),
    threading.Thread(target=second, args=("Thread2",)),
    threading.Thread(target=third),
]
for t in threads: t.start()
for t in threads: t.join()
print("Total time", datetime.datetime.now() - t1)
OUTPUT
Thread1 Thread2 Thread3 Total time 0:00:02.001270
🔧
Fixed: Total Time Is About 2 s, Not 3 sसुधार: कुल समय लगभग 2 s है, 3 s नहीं已修正:总时间约 2 秒,而不是 3 秒

The original showed Total time 0:00:03. Our real run took 2.00 s. Thread1 holds the lock from 0 to 1 s and Thread2 from 1 to 2 s. third() sleeps 2 s without the lock — at the same time as the others — and gets the lock at about 2 s, when Thread2 releases it. Its own work inside the lock is instant.

मूल में Total time 0:00:03 दिखाया गया था। हमारा असली run 2.00 s चला। Thread1 0 से 1 s तक lock रखता है और Thread2 1 से 2 s तक। third() lock के बिना 2 s sleep करता है — दूसरों के साथ-साथ — और लगभग 2 s पर, जब Thread2 छोड़ता है, lock पाता है। Lock के अंदर उसका अपना काम तुरंत होता है।

原文显示 Total time 0:00:03。我们的真实运行用时 2.00 秒。Thread1 在 0 到 1 秒持有锁,Thread2 在 1 到 2 秒持有。third() 在 不持有 锁的情况下 sleep 2 秒 — 与其他线程同时进行 — 并在约 2 秒、Thread2 释放时拿到锁。它在锁内的工作瞬间完成。

🔑
Two Lessons From Two Problemsदो समस्याओं से दो सबक़两个问题,两个教训

Problem #1 shows why RLock exists — nested acquires by the same thread. Problem #2 shows what RLock cannot do — it cannot fix code that splits acquire and release across threads, or never releases at all. The cure for both is the same: with lock:, so acquire and release always live in the same block and run in the same thread.

समस्या #1 बताती है कि RLock क्यों है — उसी thread के nested acquires। समस्या #2 बताती है कि RLock क्या नहीं कर सकता — यह उस code को ठीक नहीं कर सकता जो acquire और release को threads में बाँटता है, या कभी release नहीं करता। दोनों का इलाज एक है: with lock:, ताकि acquire और release हमेशा एक ही block में रहें और एक ही thread में चलें।

问题 #1 说明了 RLock 为什么 存在 — 同一线程的嵌套获取。问题 #2 说明了 RLock 做不到什么 — 它无法修复把获取和释放拆到不同线程、或者根本不释放的代码。两者的解药相同:with lock:,让获取和释放始终在同一个代码块、同一个线程中执行。

🔁 Simulator: Lock vs RLock Playground🔁 Simulator: Lock बनाम RLock Playground🔁 模拟器:Lock vs RLock 游乐场

Two threads, A and B, share one lock. Click buttons to make a thread call acquire(blocking=False) (so nothing ever hangs) or release(). Switch between Lock and RLock and compare. The rules match 300 random sequences we ran with real threads.

दो threads, A और B, एक lock बाँटते हैं। Buttons click करके किसी thread से acquire(blocking=False) (ताकि कुछ अटके नहीं) या release() बुलवाइए। Lock और RLock के बीच बदलकर तुलना कीजिए। ये नियम असली threads के साथ चलाए गए 300 random sequences से मिलते हैं।

两个线程 A 和 B 共享一把锁。点击按钮让某个线程调用 acquire(blocking=False)(这样不会卡住)或 release()。在 Lock 和 RLock 之间切换并比较。这些规则与我们用真实线程运行的 300 个随机序列完全一致。

Lock vs RLock PlaygroundLock बनाम RLock PlaygroundLock vs RLock 游乐场checked on 300 real sequences300 असली sequences पर जाँचा已用 300 个真实序列验证
Lock type:Lock प्रकार:锁类型:
CodeCode代码
Return valuesReturn values返回值

SectionSection第 07 节

Lock vs RLock — Side by Side ⚖️Lock बनाम RLock — आमने-सामने ⚖️Lock 与 RLock — 并排对比 ⚖️

Propertyगुण特性LockRLock
Same thread acquires againवही thread फिर acquire करे同一线程再次获取Blocks for ever (self-deadlock)हमेशा रुकता है (self-deadlock)永久阻塞(自我死锁)Works — the counter goes upचलता है — counter बढ़ता है可以 — 计数器加 1
Another thread acquiresदूसरा thread acquire करे其他线程获取Waits (correct)इंतज़ार (सही)等待(正确)Waits (correct)इंतज़ार (सही)等待(正确)
Who may releaseकौन release कर सकता है谁可以释放Any threadकोई भी thread任何线程Only the owner threadसिर्फ़ मालिक thread只有拥有者线程
Releases needed to free itमुक्त करने को कितने release释放所需次数1As many as the acquiresजितने acquires, उतने与获取次数相同
Speed (our run)गति (हमारा run)速度(我们的运行)Faster — 0.11 s per millionतेज़ — 0.11 s प्रति दस लाख更快 — 每百万次 0.11 秒About 1.5× slower — 0.16 s per millionलगभग 1.5× धीमा — 0.16 s प्रति दस लाख约慢 1.5 倍 — 每百万次 0.16 秒
Use whenकब लें适用场景One flat critical sectionएक सीधा critical section单一、扁平的临界区Nested calls, recursion, a method calling another locked methodNested calls, recursion, एक method जो दूसरा locked method बुलाए嵌套调用、递归、一个方法调用另一个加锁方法
import timeit

for kind in ("Lock", "RLock"):
    t = timeit.timeit("with lock: pass",
                      setup=f"import threading; lock = threading.{kind}()",
                      number=1_000_000)
    print(f"{kind:5s}: 1,000,000 acquire+release in {t:.2f} s")
OUTPUT
Lock : 1,000,000 acquire+release in 0.11 s RLock: 1,000,000 acquire+release in 0.16 s
🔧
Fixed: RLock Is About 1.5× Slower, Not 2×सुधार: RLock लगभग 1.5× धीमा है, 2× नहीं已修正:RLock 约慢 1.5 倍,而不是 2 倍

The original table said RLock is "~2× slower". We measured one million with lock: pass rounds: 0.11 s for Lock and 0.16 s for RLock on Python 3.13.16 — about 1.5×. The exact ratio depends on the machine and the Python version; either way it is a fraction of a microsecond per use, which almost never matters next to the real work inside the lock.

मूल table में RLock को "~2× slower" कहा गया था। हमने दस लाख with lock: pass rounds मापे: Python 3.13.16 पर Lock के लिए 0.11 s और RLock के लिए 0.16 s — लगभग 1.5×। सटीक अनुपात machine और Python version पर निर्भर है; दोनों ही हालात में यह हर इस्तेमाल पर microsecond का एक हिस्सा है, जो lock के अंदर के असली काम के सामने लगभग कभी मायने नहीं रखता।

原表格说 RLock "约慢 2 倍"。我们测量了一百万次 with lock: pass:在 Python 3.13.16 上 Lock 用时 0.11 秒,RLock 用时 0.16 秒 — 约 1.5 倍。具体比例取决于机器和 Python 版本;无论如何,每次使用都只差不到一微秒,与锁内的实际工作相比几乎可以忽略。

🔒
Use Lock WhenLock कब लें何时用 Lock
simple, flatसरल, सीधा简单、扁平
One critical section, no nested calls, no recursion. The simplest and fastest choice for "just protect this counter".
एक critical section, कोई nested call नहीं, कोई recursion नहीं। "बस इस counter को बचाओ" के लिए सबसे सरल और तेज़ चुनाव।
只有一个临界区,没有嵌套调用,没有递归。对于"只是保护这个计数器",这是最简单、最快的选择。
🔁
Use RLock WhenRLock कब लें何时用 RLock
nested, recursivenested, recursive嵌套、递归
Class methods that call other locked methods, recursive functions, or when you cannot be sure whether a helper takes the same lock too.
Class methods जो दूसरे locked methods बुलाते हैं, recursive functions, या जब पक्का न हो कि कोई helper भी वही lock लेता है।
调用其他加锁方法的类方法、递归函数,或者你不确定某个辅助函数是否也会获取同一把锁时。
🧠
When in Doubtशक हो तो拿不准时
know your call pathsअपने call paths जानिए弄清调用路径
If a locked function might call another function that takes the same lock, RLock is the safer choice — the speed cost is tiny. But RLock is not a cure-all: it does not stop deadlocks between different locks, and it hides how deep your nesting goes.
अगर कोई locked function ऐसा function बुला सकता है जो वही lock लेता है, तो RLock सुरक्षित चुनाव है — गति की क़ीमत बहुत छोटी है। पर RLock हर मर्ज़ की दवा नहीं: यह अलग-अलग locks के बीच के deadlocks नहीं रोकता, और nesting कितनी गहरी है यह छिपा देता है।
如果一个加锁的函数可能调用另一个获取同一把锁的函数,RLock 是更安全的选择 — 速度代价很小。但 RLock 不是万能药:它无法阻止 不同 锁之间的死锁,还会掩盖嵌套到底有多深。
🔧
Fixed: "Modern Code Uses RLock More Often" Is Not a Ruleसुधार: "आधुनिक Code ज़्यादा RLock लेता है" कोई नियम नहीं已修正:"现代代码更常用 RLock" 并不是规则

The original said "prefer RLock" and that modern code uses RLock more often than Lock. There is no such rule — plain Lock remains the usual default for a simple critical section. Choose RLock for a real reason: the same thread may need the same lock again.

मूल में कहा गया "RLock को प्राथमिकता दें" और आधुनिक code Lock से ज़्यादा RLock लेता है। ऐसा कोई नियम नहीं — साधारण critical section के लिए सामान्य default आज भी Lock ही है। RLock किसी असली कारण से चुनिए: वही thread फिर वही lock माँग सकता है।

原文说"优先使用 RLock",并称现代代码比 Lock 更常用 RLock。并没有这样的规则 — 对于简单的临界区,普通的 Lock 依然是常用的默认选择。选择 RLock 要有真实的理由:同一线程可能需要再次获取同一把锁。


SectionSection第 08 节

Real-World Example — A Thread-Safe Bank Account 🏦असली उदाहरण — Thread-Safe Bank Account 🏦实战示例 — 线程安全的银行账户 🏦

A BankAccount class that many threads can use at once. transfer() calls withdraw() and deposit(), and all three take the account's lock — nested locking, so RLock is the natural choice. Three customers each do five rounds of deposit, withdraw and transfer.

एक BankAccount class जिसे कई threads एक साथ इस्तेमाल कर सकते हैं। transfer() withdraw() और deposit() बुलाता है, और तीनों account का lock लेते हैं — nested locking, इसलिए RLock स्वाभाविक चुनाव है। तीन customers हर एक deposit, withdraw और transfer के पाँच rounds करते हैं।

一个可以被多个线程同时使用的 BankAccount 类。transfer() 会调用 withdraw() 和 deposit(),三者都会获取账户的锁 — 这是嵌套加锁,所以 RLock 是自然的选择。三个客户各自进行五轮存款、取款和转账。

import threading, time

class BankAccount:
    def __init__(self, name, balance=0):
        self.name = name
        self.balance = balance
        self.lock = threading.RLock()      # reentrant - allows nesting

    def deposit(self, amount):
        with self.lock:
            new_bal = self.balance + amount
            time.sleep(0.001)              # simulate DB latency
            self.balance = new_bal

    def withdraw(self, amount):
        with self.lock:
            if amount > self.balance:
                raise ValueError(f"{self.name}: insufficient funds")
            new_bal = self.balance - amount
            time.sleep(0.001)
            self.balance = new_bal

    def transfer(self, to_account, amount):
        with self.lock:                    # outer lock on source
            with to_account.lock:          # lock on destination
                self.withdraw(amount)      # re-acquires self.lock - OK with RLock
                to_account.deposit(amount) # re-acquires to_account.lock - OK

alice = BankAccount("alice", 1000)
bob = BankAccount("bob", 1000)

def busy_customer():
    for _ in range(5):
        alice.deposit(10)          # money comes IN from outside
        bob.withdraw(5)            # money goes OUT to the customer
        alice.transfer(bob, 20)    # money moves INSIDE the bank

threads = [threading.Thread(target=busy_customer, name=f"cust-{i}") for i in range(3)]
for t in threads: t.start()
for t in threads: t.join()

expected = 2000 + 3 * 5 * 10 - 3 * 5 * 5
print(f"final:  alice={alice.balance}  bob={bob.balance}")
print(f"total:  {alice.balance + bob.balance}   (expected {expected}: 2000 + 150 deposited - 75 withdrawn)")
OUTPUT
final: alice=850 bob=1225 total: 2075 (expected 2075: 2000 + 150 deposited - 75 withdrawn)
🔧
Fixed: The Total Is 2075, Not 2000सुधार: कुल 2075 है, 2000 नहीं已修正:总额是 2075,而不是 2000

The original said the total "should be 2000 — money is conserved" and showed alice=850 bob=1150. But busy_customer() also deposits money from outside (+10 × 15 = +150) and withdraws money to the customer (−5 × 15 = −75). Only transfer() conserves money. The correct total is 2000 + 150 − 75 = 2075, and the real run gives alice=850 bob=1225. (850 + 1150 = 2000 was impossible with this code.)

मूल में कहा गया कि कुल "2000 होना चाहिए — पैसा बचा रहता है" और alice=850 bob=1150 दिखाया गया। पर busy_customer() बाहर से पैसा भी जमा करता है (+10 × 15 = +150) और customer को पैसा निकालकर भी देता है (−5 × 15 = −75)। सिर्फ़ transfer() पैसा बचाए रखता है। सही कुल 2000 + 150 − 75 = 2075 है, और असली run alice=850 bob=1225 देता है। (इस code से 850 + 1150 = 2000 असंभव था।)

原文说总额"应该是 2000 — 资金守恒",并展示了 alice=850 bob=1150。但 busy_customer() 还会从外部存入资金(+10 × 15 = +150),并向客户取出资金(−5 × 15 = −75)。只有 transfer() 是资金守恒的。正确总额是 2000 + 150 − 75 = 2075,真实运行得到 alice=850 bob=1225。(用这段代码,850 + 1150 = 2000 是不可能的。)

Now the same program with every lock removed (with self.lock: replaced by if True:). Same customers, same amounts:

अब वही program हर lock हटाकर (with self.lock: की जगह if True:)। वही customers, वही रक़में:

现在是去掉所有锁的同一个程序(把 with self.lock: 换成 if True:)。同样的客户,同样的金额:

OUTPUT — NO LOCKSOUTPUT — बिना LOCKS输出 — 不加锁
final: alice=950 bob=1075 total: 2025 (expected 2075: 2000 + 150 deposited - 75 withdrawn)
🏆
Why This Worksयह क्यों चलता है为什么这样可行

Without locks, the time.sleep(0.001) between reading and writing the balance lets other threads overwrite it — 50 units simply vanished in our run. With an RLock per account, every read-modify-write is protected, and transfer() can call withdraw() and deposit(), which take the same locks again, without blocking itself.

Locks के बिना, balance पढ़ने और लिखने के बीच का time.sleep(0.001) दूसरे threads को उसे overwrite करने देता है — हमारे run में 50 units बस ग़ायब हो गए। हर account पर एक RLock के साथ हर read-modify-write सुरक्षित है, और transfer() withdraw() और deposit() बुला सकता है, जो फिर वही locks लेते हैं, ख़ुद को रोके बिना।

不加锁时,读取和写回余额之间的 time.sleep(0.001) 让其他线程有机会覆盖它 — 在我们的运行中 50 个单位就这样凭空消失了。给每个账户一把 RLock 后,每次读-改-写都受到保护,transfer() 可以调用会再次获取相同锁的 withdraw() 和 deposit(),而不会阻塞自己。

⚠️
Hidden Deadlock: Transfers in Opposite Directionsछिपा Deadlock: उलटी दिशाओं में Transfers隐藏的死锁:反方向转账

This example only transfers from alice to bob. If one thread ran alice.transfer(bob, …) while another ran bob.transfer(alice, …), the first would hold alice's lock and wait for bob's, while the second holds bob's and waits for alice's — the deadlock from Section 05. RLock does not help here, because these are two different locks. The fix is the lock-ordering rule: always lock the two accounts in a fixed order (for example, by name).

यह example सिर्फ़ alice से bob को transfer करता है। अगर एक thread alice.transfer(bob, …) चलाए और दूसरा bob.transfer(alice, …), तो पहला alice का lock रखकर bob के lock का इंतज़ार करेगा, और दूसरा bob का रखकर alice के का — Section 05 वाला deadlock। RLock यहाँ मदद नहीं करता, क्योंकि ये दो अलग locks हैं। इलाज lock-ordering नियम है: दोनों accounts को हमेशा एक तय क्रम में lock कीजिए (जैसे नाम के क्रम में)।

这个例子只从 alice 转账给 bob。如果一个线程运行 alice.transfer(bob, …),另一个线程同时运行 bob.transfer(alice, …),第一个会持有 alice 的锁并等待 bob 的锁,第二个持有 bob 的锁并等待 alice 的锁 — 正是第 05 节的死锁。RLock 在这里帮不上忙,因为这是两把 不同 的锁。解决办法是加锁顺序规则:总是以固定顺序锁住两个账户(例如按名字排序)。


SectionSection第 09 节

Common Pitfalls and the Bug Doctor 🩺आम ग़लतियाँ और Bug Doctor 🩺常见陷阱与 Bug 医生 🩺

❌
Forgetting to release()release() भूलना忘记 release()
use a with blockwith block लीजिए使用 with 块
An exception between acquire() and release() leaves the lock held. Every thread that waits for it waits for ever. Use with lock:.
acquire() और release() के बीच exception lock को पकड़ा हुआ छोड़ देता है। उसका इंतज़ार करने वाला हर thread हमेशा इंतज़ार करता है। with lock: लीजिए।
acquire() 和 release() 之间的异常会让锁一直被持有。所有等待它的线程都会永远等待。请使用 with lock:。
❌
Setting daemon after start()start() के बाद daemon set करना在 start() 之后设置 daemon
set it before startstart से पहले set在 start 之前设置
Raises RuntimeError. Pass daemon=True to the constructor, or set it right after Thread(...).
RuntimeError raise होता है। Constructor में daemon=True दीजिए, या Thread(...) के ठीक बाद set कीजिए।
会抛出 RuntimeError。请在构造函数中传入 daemon=True,或在 Thread(...) 之后立即设置。
❌
Different lock order in different threadsअलग threads में अलग lock क्रम不同线程以不同顺序加锁
one global orderएक global क्रम统一的全局顺序
The classic deadlock recipe. Always acquire several locks in the same order in every thread.
Classic deadlock का नुस्ख़ा। हर thread में कई locks हमेशा एक ही क्रम में acquire कीजिए।
经典的死锁配方。在每个线程中,总是以 相同 的顺序获取多把锁。
✅
RLock for method chainsMethod chains के लिए RLock方法链用 RLock
nested lockingnested locking嵌套加锁
If a locked method may call another method that takes the same lock, use RLock. It is cheaper than debugging a self-deadlock.
अगर कोई locked method ऐसा method बुला सकता है जो वही lock लेता है, तो RLock लीजिए। Self-deadlock debug करने से यह सस्ता है।
如果一个加锁的方法可能调用另一个获取同一把锁的方法,就用 RLock。这比调试自我死锁省事得多。
✅
queue.Queue for producer/consumerProducer/consumer के लिए queue.Queue生产者/消费者用 queue.Queue
already thread-safeपहले से thread-safe本身就线程安全
queue.Queue (from the queue tutorial) already does its own locking. Use it instead of a hand-made lock-protected list.
queue.Queue (queue tutorial वाला) अपनी locking ख़ुद करता है। हाथ से बनी lock-protected list की जगह इसे लीजिए।
queue.Queue(队列教程中介绍过)自带加锁。用它代替手写的、靠锁保护的列表。
✅
Keep critical sections shortCritical sections छोटे रखिए保持临界区简短
less waitingकम इंतज़ार减少等待
Put only the shared-data operations inside with lock:. Slow I/O, logging and heavy computation belong outside, so other threads do not wait for nothing.
with lock: के अंदर सिर्फ़ साझा-data operations रखिए। धीमा I/O, logging और भारी गणना बाहर रखिए, ताकि दूसरे threads बेवजह इंतज़ार न करें।
with lock: 中只放共享数据的操作。缓慢的 I/O、日志和繁重计算应放在锁外,免得其他线程白白等待。

🩺 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 given daemon threads or timeouts so they could report).

इस tutorial की छह असली ग़लतियाँ। बाईं ओर buggy code, दाईं ओर ठीक किया हुआ, बदली lines चिह्नित। हर output असली run से है (जो programs अटक जाते उन्हें daemon threads या timeouts दिए गए ताकि वे नतीजा बता सकें)।

本教程中的六个真实错误。左边是有 bug 的代码,右边是修正后的代码,改动的行已标出。每个输出都来自真实运行(会挂起的程序被改用守护线程或超时,以便能报告结果)。

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

SectionSection第 10 节

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

🌲 Daemon Threads & Synchronization — Rules to RememberDaemon Threads और Synchronization — याद रखने वाले नियम守护线程与同步 — 必须记住的规则
1
Use daemon=True only for background helpers that hold nothing important — heartbeats, monitors, tickers. Never for threads that write files, hold database transactions or must clean up: at exit they are cut off with no finally and no buffer flush.
daemon=True सिर्फ़ उन background सहायकों के लिए जो कुछ ज़रूरी नहीं रखते — heartbeats, monitors, tickers। Files लिखने, database transactions रखने या सफ़ाई करने वाले threads के लिए कभी नहीं: exit पर वे बिना finally और बिना buffer flush के काट दिए जाते हैं।
daemon=True 只用于不保存重要数据的后台助手 — 心跳、监控、计时器。绝不要用于写文件、持有数据库事务或必须清理的线程:退出时它们会被直接切断,不运行 finally,也不刷新缓冲区。
2
Set daemon in the constructor or right after Thread(...) — before start(). Changing it later raises RuntimeError. Prefer daemon=True over the deprecated setDaemon().
daemon constructor में या Thread(...) के ठीक बाद set कीजिए — start() से पहले। बाद में बदलने पर RuntimeError आता है। Deprecated setDaemon() की जगह daemon=True लीजिए।
在构造函数中或 Thread(...) 之后立即设置 daemon — 在 start() 之前。之后再改会抛出 RuntimeError。用 daemon=True 代替已弃用的 setDaemon()。
3
Any variable that more than one thread reads and writes must be protected by a lock. Even counter += 1 is read, add, write — and a run that happens to come out right proves nothing.
जिस variable को एक से ज़्यादा threads पढ़ते और लिखते हैं, उसे lock से बचाना ज़रूरी है। counter += 1 भी read, add, write है — और संयोग से सही निकला run कुछ साबित नहीं करता।
任何被多个线程既读 又 写的变量都必须用锁保护。即使是 counter += 1 也是读、加、写三步 — 碰巧结果正确的一次运行什么也证明不了。
4
Use with lock: instead of manual acquire()/release(). It releases the lock even if the code inside raises an exception.
हाथ से acquire()/release() की जगह with lock: लीजिए। अंदर का code exception raise करे तब भी यह lock release करता है।
用 with lock: 代替手动的 acquire()/release()。即使内部代码抛出异常,它也会释放锁。
5
Keep critical sections short. Only the shared-data operations belong inside the lock; slow I/O, logging and heavy computation go outside.
Critical sections छोटे रखिए। Lock के अंदर सिर्फ़ साझा-data operations; धीमा I/O, logging और भारी गणना बाहर।
保持临界区简短。锁内只放共享数据操作;缓慢的 I/O、日志和繁重计算放在锁外。
6
When a thread needs several locks, every thread must acquire them in the same global order. If everyone takes lock1 before lock2, no cycle can form. A timeout on acquire() is a second safety net.
जब किसी thread को कई locks चाहिए, तो हर thread उन्हें एक ही global क्रम में acquire करे। सब lock1 को lock2 से पहले लें तो चक्र नहीं बन सकता। acquire() पर timeout दूसरा सुरक्षा जाल है।
当线程需要多把锁时,所有线程都必须以 相同的全局顺序 获取它们。如果大家都先拿 lock1 再拿 lock2,就不会形成环。给 acquire() 加 timeout 是第二道安全网。
7
Use RLock when a locked function may call another function that takes the same lock — nested calls, recursion, method chains. A plain Lock would block the thread against itself.
जब कोई locked function ऐसा function बुला सकता है जो वही lock लेता है — nested calls, recursion, method chains — तब RLock लीजिए। साधारण Lock thread को ख़ुद से ही रोक देगा।
当一个加锁的函数可能调用另一个获取同一把锁的函数时 — 嵌套调用、递归、方法链 — 使用 RLock。普通的 Lock 会让线程被自己阻塞。
8
Only the thread that acquired an RLock may release it, and it must release it as many times as it acquired it. Too few releases leave the lock held for ever.
RLock सिर्फ़ वही thread release कर सकता है जिसने acquire किया, और उतनी ही बार release करे जितनी बार acquire किया। कम releases lock को हमेशा के लिए पकड़ा छोड़ देते हैं।
只有获取了 RLock 的线程才能释放它,而且释放次数必须与获取次数相同。释放次数不足会让锁永远被持有。
9
For producer/consumer work, prefer queue.Queue over a hand-made lock-protected list. It is already thread-safe and has blocking put()/get().
Producer/consumer काम के लिए हाथ से बनी lock-protected list की जगह queue.Queue लीजिए। यह पहले से thread-safe है और इसमें blocking put()/get() हैं।
对于生产者/消费者工作,优先使用 queue.Queue 而不是手写的、靠锁保护的列表。它本身就是线程安全的,并提供阻塞式的 put()/get()。

Quick Quiz — 8 Questionsछोटा Quiz — 8 सवाल小测验 — 8 道题

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

Daemon threads are background helpers that are cut off when the last non-daemon thread ends — fine for heartbeats, dangerous for anything that writes. Whenever threads share data that they also change, protect it with a lock using with lock:. Take several locks in one fixed order to avoid deadlock, and reach for RLock only when the same thread really needs the same lock again.

Daemon threads background सहायक हैं जो आख़िरी non-daemon thread ख़त्म होते ही काट दिए जाते हैं — heartbeats के लिए ठीक, लिखने वाले किसी भी काम के लिए ख़तरनाक। जब भी threads ऐसा data बाँटें जिसे वे बदलते भी हैं, उसे with lock: से बचाइए। Deadlock से बचने के लिए कई locks एक तय क्रम में लीजिए, और RLock तभी लीजिए जब वही thread सच में वही lock फिर माँगे।

守护线程是后台助手,最后一个非守护线程结束时它们就会被切断 — 适合心跳,但对任何写入操作都很危险。只要线程共享并修改数据,就用 with lock: 加锁保护。以固定顺序获取多把锁以避免死锁,并且只有在同一线程确实需要再次获取同一把锁时才使用 RLock。

🐍 Try Python Yourself

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

You have completed Threads. View all sections →