The Story That Explains Synchronization 📝Synchronization समझाने वाली कहानी 📝一个讲清同步的故事 📝
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.
यही 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 बाँटने देना।
这就是 竞态条件:两个工作者在没有协调的情况下读写同一份共享数据。在 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 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 अपनी बारी का इंतज़ार करता है।
只要两个或更多线程既读 又 写同一份数据,你就遇到了同步问题。锁通过把许多重叠的写操作变成单列队伍来解决它,每个线程都要等轮到自己。
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)。它们的输出都来自真实运行。
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 बंद होते समय दिखता है:
每个线程要么是 非守护 线程(默认),要么是 守护 线程。区别只在程序关闭时才显现:
| 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(或不设置) |
| 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() 之前设置 |
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")
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()
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()
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 速度)。
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()
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")
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")。所以真正的危险更大:你可能丢失 全部 数据,而不只是结尾部分。
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 清理,不刷新缓冲区。凡是写文件、持有数据库事务或必须清理的工作,绝不要用守护线程。守护线程只适合不保存任何重要数据的后台循环。
join().join() लीजिए।join()。join() it, so it can finish cleanly. The threading.Event tool for this is covered in an upcoming tutorial.join() कीजिए, ताकि वह साफ़ ख़त्म हो। इसका औज़ार threading.Event एक आने वाले tutorial में है।join() 它,让它干净地结束。用于此的工具 threading.Event 会在 后续教程 中介绍。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 करते हैं और उनके क़दमों के आपस में मिलने से ग़लत नतीजा आता है।
在介绍锁之前,先看看它们要解决的问题。当两个或更多线程对同一个值进行"读-改-写",而它们的步骤交错执行导致结果错误时,就发生了 竞态条件。
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")
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")
👣 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() 的线程会被阻塞。
📊 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 上运行了五次。绿色条达到了预期总数;红色条丢失了更新。
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% 的更新就丢失了。加锁后,每次运行都完全正确。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:它不会崩溃,而且由于线程的时序每次运行都不同,它可能在测试中看起来没问题,只在真实负载下才出错。
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()。
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}")
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() — 即使发生异常也一样。
lock.acquire()
risky_operation() # may raise
lock.release() # never runs on an exception!| The lock leaks → other threads wait for everLock leak → बाक़ी threads हमेशा इंतज़ार锁泄漏 → 其他线程永远等待 |
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}")
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只尝试一次;如果锁已被占用则立即返回 False | Skip work if busyव्यस्त हो तो काम छोड़ें忙时跳过工作 |
lock.acquire(timeout=2) | Waits up to 2 s; returns False on timeout2 s तक इंतज़ार; timeout पर False最多等待 2 秒;超时返回 False | Avoid 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如果此刻有线程持有锁则为 True | Diagnostics onlyसिर्फ़ जाँच के लिए仅用于诊断 |
with lock: | Acquire on entry, release on exit — even on an exceptionअंदर जाते acquire, निकलते release — exception पर भी进入时获取,退出时释放 — 异常时也一样 | Always prefer thisहमेशा यही始终优先使用 |
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()
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 中释放。
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 बस जम जाता है।
当两个或更多线程各自等待另一个线程持有的锁时,就发生了 死锁。没有崩溃,也没有输出 — 程序就这么冻住了。
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
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।
双锁程序的三个版本,每个都真实运行过(事件以秒为单位)。观察锁关系图:绿线表示"持有",红色虚线箭头表示"等待"。红色的环就是死锁。
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):拿不到第二把锁的线程会放弃,释放已持有的锁,稍等片刻再重试 — 也就是"超时"运行。
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(可重入锁)解决了这个问题:它记录 哪个线程拥有它,以及该线程 获取了多少次。
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
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")
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()
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。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。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。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。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。现在锁才真正空闲,其他线程可以获取它。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" 之后 |
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)— 输出全部打印 |
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)
second("Thread1"): acquires, prints "Thread1", and its function returns without releasing. th1 ends — but still owns the lock.second("Thread1") चलाता है: acquire करता है, "Thread1" print करता है, और function release किए बिना लौट आता है। th1 ख़त्म — पर lock का मालिक अब भी वही।second("Thread1"):获取锁,打印 "Thread1",函数在 没有释放 的情况下返回。th1 结束了 — 但仍然拥有这把锁。second("Thread2"): acquire() blocks — the lock belongs to th1.second("Thread2") चलाता है: acquire() रुक जाता है — lock th1 का है।second("Thread2"):acquire() 被阻塞 — 锁属于 th1。lock.release(), but th3 never acquired it → RuntimeError: cannot release un-acquired lock. th3 dies; "Thread3" is never printed.lock.release() बुलाता है, पर th3 ने इसे कभी acquire नहीं किया → RuntimeError: cannot release un-acquired lock। th3 मर जाता है; "Thread3" कभी print नहीं होता।lock.release(),但 th3 从未获取过它 → RuntimeError: cannot release un-acquired lock。th3 终止;"Thread3" 永远不会被打印。th2.join() in main waits for ever and "Total time" never prints. (We stopped it after 7 s.)th2.join() हमेशा इंतज़ार करता है और "Total time" कभी print नहीं होता। (हमने 7 s बाद रोका।)th2.join() 永远等待,"Total time" 永远不会打印。(我们在 7 秒后停止了它。)(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)
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 释放时拿到锁。它在锁内的工作瞬间完成。
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 — Side by Side ⚖️Lock बनाम RLock — आमने-सामने ⚖️Lock 与 RLock — 并排对比 ⚖️
| Propertyगुण特性 | Lock | RLock |
|---|---|---|
| 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释放所需次数 | 1 | As 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")
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 版本;无论如何,每次使用都只差不到一微秒,与锁内的实际工作相比几乎可以忽略。
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 要有真实的理由:同一线程可能需要再次获取同一把锁。
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)")
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:)。同样的客户,同样的金额:
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(),而不会阻塞自己。
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 在这里帮不上忙,因为这是两把 不同 的锁。解决办法是加锁顺序规则:总是以固定顺序锁住两个账户(例如按名字排序)。
Common Pitfalls and the Bug Doctor 🩺आम ग़लतियाँ और Bug Doctor 🩺常见陷阱与 Bug 医生 🩺
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:。RuntimeError. Pass daemon=True to the constructor, or set it right after Thread(...).RuntimeError raise होता है। Constructor में daemon=True दीजिए, या Thread(...) के ठीक बाद set कीजिए।RuntimeError。请在构造函数中传入 daemon=True,或在 Thread(...) 之后立即设置。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(队列教程中介绍过)自带加锁。用它代替手写的、靠锁保护的列表。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 的代码,右边是修正后的代码,改动的行已标出。每个输出都来自真实运行(会挂起的程序被改用守护线程或超时,以便能报告结果)。
Golden Rules and Quiz 🏆सुनहरे नियम और Quiz 🏆黄金法则与小测验 🏆
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,也不刷新缓冲区。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()。counter += 1 is read, add, write — and a run that happens to come out right proves nothing.counter += 1 भी read, add, write है — और संयोग से सही निकला run कुछ साबित नहीं करता।counter += 1 也是读、加、写三步 — 碰巧结果正确的一次运行什么也证明不了。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()。即使内部代码抛出异常,它也会释放锁。lock1 before lock2, no cycle can form. A timeout on acquire() is a second safety net.lock1 को lock2 से पहले लें तो चक्र नहीं बन सकता। acquire() पर timeout दूसरा सुरक्षा जाल है।lock1 再拿 lock2,就不会形成环。给 acquire() 加 timeout 是第二道安全网。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.RLock लीजिए। साधारण Lock thread को ख़ुद से ही रोक देगा।RLock。普通的 Lock 会让线程被自己阻塞。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 的线程才能释放它,而且释放次数必须与获取次数相同。释放次数不足会让锁永远被持有。queue.Queue over a hand-made lock-protected list. It is already thread-safe and has blocking put()/get().queue.Queue लीजिए। यह पहले से thread-safe है और इसमें blocking put()/get() हैं।queue.Queue 而不是手写的、靠锁保护的列表。它本身就是线程安全的,并提供阻塞式的 put()/get()。Quick Quiz — 8 Questionsछोटा Quiz — 8 सवाल小测验 — 8 道题
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。