深入理解 Python 中的 asyncio.Lock
一、為什么在 asyncio 里仍然需要鎖
很多初學(xué)者第一次接觸 asyncio.Lock 時(shí)會(huì)有一個(gè)典型疑問(wèn):asyncio 明明運(yùn)行在單線程事件循環(huán)上,為什么還會(huì)需要鎖?
這個(gè)疑問(wèn)的根源在于把“單線程”誤解成了“不會(huì)發(fā)生并發(fā)沖突”。事實(shí)上,asyncio 雖然通常不依賴多線程并行執(zhí)行 Python 字節(jié)碼,但它仍然存在協(xié)作式并發(fā)。只要多個(gè)協(xié)程會(huì)在同一時(shí)間段內(nèi)訪問(wèn)共享狀態(tài),并且這些訪問(wèn)過(guò)程跨越了 await 掛起點(diǎn),就可能出現(xiàn)競(jìng)態(tài)條件。
換句話說(shuō),asyncio.Lock 不是用來(lái)解決“多線程搶占 CPU”的問(wèn)題,而是用來(lái)解決“多個(gè)協(xié)程在調(diào)度切換下交錯(cuò)訪問(wèn)共享資源”的問(wèn)題。
嚴(yán)格地說(shuō),鎖保護(hù)的不是代碼片段本身,而是某個(gè)必須保持一致性的共享狀態(tài)及其不變量。
二、asyncio.Lock的職責(zé)邊界
asyncio.Lock 是 asyncio 提供的最基礎(chǔ)互斥原語(yǔ)之一。它的核心語(yǔ)義很簡(jiǎn)單:
- 同一時(shí)刻最多只允許一個(gè)協(xié)程進(jìn)入臨界區(qū)。
- 若鎖已被占用,后續(xù)協(xié)程在
acquire()處掛起,等待鎖釋放。 - 鎖本身不提供讀寫(xiě)區(qū)分、條件通知、計(jì)數(shù)許可等更高層能力,它只負(fù)責(zé)最基本的互斥。
因此,它最適合用來(lái)保護(hù)如下對(duì)象:
- 共享內(nèi)存狀態(tài),例如計(jì)數(shù)器、緩存字典、會(huì)話表、批量緩沖區(qū)。
- 需要串行訪問(wèn)的外部資源包裝層,例如某個(gè)連接對(duì)象、寫(xiě)日志出口、帶狀態(tài)協(xié)議客戶端。
- 需要維護(hù)操作原子性的多步流程,例如“讀舊值、計(jì)算新值、寫(xiě)回結(jié)果”這一類(lèi)復(fù)合更新。
同時(shí)也要明確,asyncio.Lock 不負(fù)責(zé)以下事情:
- 它不能替代隊(duì)列、事件、信號(hào)量等其他同步原語(yǔ)。
- 它不能解決 CPU 密集型阻塞造成的事件循環(huán)卡頓。
- 它不能跨線程或跨進(jìn)程提供可靠同步;它服務(wù)的是當(dāng)前事件循環(huán)內(nèi)的協(xié)程協(xié)作。
三、鎖解決的本質(zhì)問(wèn)題:臨界區(qū)與不變量
理解 asyncio.Lock,關(guān)鍵不在 API,而在“臨界區(qū)”這個(gè)概念。所謂臨界區(qū),是指一段在執(zhí)行期間不能被其他并發(fā)任務(wù)打斷其共享狀態(tài)一致性的邏輯。
例如,一個(gè)簡(jiǎn)單的自增操作在抽象語(yǔ)義上看似只有一句:
counter += 1
但如果實(shí)際業(yè)務(wù)邏輯中,自增過(guò)程被拆成“讀取當(dāng)前值”“基于舊值計(jì)算”“寫(xiě)回新值”三個(gè)步驟,并且中間出現(xiàn) await,那么多個(gè)協(xié)程就可能基于同一個(gè)舊值同時(shí)計(jì)算,最終導(dǎo)致寫(xiě)回覆蓋,出現(xiàn)丟失更新問(wèn)題。
因此,鎖的真正意義是:
- 將一組本應(yīng)視為一個(gè)原子操作的步驟包起來(lái)。
- 在這組步驟執(zhí)行期間,阻止其他協(xié)程觀察到中間態(tài)。
- 維護(hù)共享狀態(tài)的關(guān)鍵不變量,例如“余額不能為負(fù)”“緩存刷新期間映射關(guān)系必須完整”“同一連接寫(xiě)操作不能交錯(cuò)”。
工程上最常見(jiàn)的錯(cuò)誤不是“忘記使用鎖”,而是沒(méi)有先定義清楚到底要保護(hù)什么不變量。如果不變量本身不清楚,鎖往往會(huì)加錯(cuò)位置,最終既損失并發(fā)性能,也沒(méi)有真正消除競(jìng)態(tài)。
四、基礎(chǔ)用法:acquire()、release()與async with
asyncio.Lock 的使用方式有兩種。
第一種是顯式調(diào)用:
lock = asyncio.Lock()
await lock.acquire()
try:
# 臨界區(qū)
...
finally:
lock.release()
第二種也是更推薦的方式,是上下文管理協(xié)議:
lock = asyncio.Lock()
async with lock:
# 臨界區(qū)
...
兩者在語(yǔ)義上等價(jià),但 async with lock 更安全,原因有三點(diǎn):
- 結(jié)構(gòu)更清晰,讀者一眼就能識(shí)別臨界區(qū)范圍。
- 自動(dòng)配合異常路徑釋放鎖,降低遺漏
release()的風(fēng)險(xiǎn)。 - 代碼更短,審查成本更低。
除非你確實(shí)需要更精細(xì)的獲取與釋放時(shí)機(jī)控制,否則應(yīng)優(yōu)先使用 async with。對(duì)于大多數(shù)工程代碼,這是更穩(wěn)妥的寫(xiě)法。
五、一個(gè)典型例子:為什么單線程協(xié)程也會(huì)發(fā)生競(jìng)態(tài)
下面這個(gè)模式在異步代碼中并不少見(jiàn):
import asyncio
counter = 0
async def increment():
global counter
current = counter
await asyncio.sleep(0)
counter = current + 1
async def main():
await asyncio.gather(*(increment() for _ in range(1000)))
print(counter)
從直覺(jué)上看,執(zhí)行 1000 次遞增,結(jié)果似乎應(yīng)當(dāng)是 1000;但實(shí)際結(jié)果往往會(huì)小于 1000。原因并不神秘:多個(gè)協(xié)程可能先后讀到同一個(gè)舊值,再在未來(lái)某個(gè)調(diào)度時(shí)刻把各自計(jì)算出的新值寫(xiě)回,最終覆蓋彼此的更新。
改寫(xiě)方式如下:
import asyncio
counter = 0
lock = asyncio.Lock()
async def increment():
global counter
async with lock:
current = counter
await asyncio.sleep(0)
counter = current + 1
async def main():
await asyncio.gather(*(increment() for _ in range(1000)))
print(counter)
這個(gè)示例雖然刻意,但它揭示了一個(gè)非常關(guān)鍵的事實(shí):只要共享狀態(tài)讀寫(xiě)跨越了掛起點(diǎn),協(xié)程之間就可能形成真正的競(jìng)爭(zhēng)。
六、locked()能做什么,不能做什么
asyncio.Lock 提供 locked() 方法,用于查詢鎖當(dāng)前是否處于占用狀態(tài):
if lock.locked():
...
但需要強(qiáng)調(diào),locked() 通常只能用于調(diào)試、監(jiān)控或日志判斷,而不應(yīng)作為可靠控制流依據(jù)。原因是“檢查”和“獲取”之間不是原子操作。你看到鎖此刻未被占用,并不意味著下一行執(zhí)行 acquire() 時(shí)它仍然空閑。
因此,下面這種思路在并發(fā)語(yǔ)義上并不嚴(yán)謹(jǐn):
if not lock.locked():
await lock.acquire()
正確原則是:是否真正獲得進(jìn)入臨界區(qū)的資格,只能以 await lock.acquire() 或 async with lock 的結(jié)果為準(zhǔn),而不能以事前觀察為準(zhǔn)。
七、取消語(yǔ)義:等待鎖與持有鎖是兩種不同階段
在異步程序里,鎖的使用不能只看正常路徑,還必須分析取消路徑。這里至少要分清兩種階段。
1. 協(xié)程正在等待獲取鎖
當(dāng)協(xié)程阻塞在 await lock.acquire() 時(shí),如果任務(wù)被取消,通常會(huì)直接拋出 CancelledError,表示該協(xié)程放棄等待。此時(shí)它尚未進(jìn)入臨界區(qū),也通常無(wú)需負(fù)責(zé)釋放鎖。
2. 協(xié)程已經(jīng)持有鎖
一旦協(xié)程成功獲得鎖,再在臨界區(qū)內(nèi)遭遇取消,就必須確保釋放動(dòng)作一定發(fā)生。否則,其他等待者可能永久阻塞,形成邏輯死鎖。
這也是為什么 async with lock 比手工 acquire() / release() 更穩(wěn)妥。因?yàn)樗烊话厌尫艅?dòng)作放進(jìn)了可靠的退出路徑中。
如果臨界區(qū)里除了鎖外還持有網(wǎng)絡(luò)連接、文件句柄、事務(wù)對(duì)象等資源,那么還應(yīng)進(jìn)一步用 try/finally 明確資源清理策略。鎖只解決互斥,不替你設(shè)計(jì)完整的資源生命周期。
八、常見(jiàn)誤區(qū)一:把鎖持有時(shí)間拉得過(guò)長(zhǎng)
鎖的一個(gè)基本成本是:它會(huì)主動(dòng)降低并發(fā)度,以換取一致性。因此,臨界區(qū)不應(yīng)無(wú)邊界擴(kuò)張。
下面這種寫(xiě)法在語(yǔ)義上雖然正確,但在性能和吞吐上往往不理想:
async with lock:
data = await fetch_remote_data()
result = transform(data)
shared_cache[key] = result
問(wèn)題在于,遠(yuǎn)程 I/O 等待期間鎖一直被持有,其他協(xié)程即使只想執(zhí)行一個(gè)非常短的共享狀態(tài)更新,也必須排隊(duì)。更合理的思路通常是:
- 在鎖外完成無(wú)需共享保護(hù)的耗時(shí)工作。
- 只把真正涉及共享狀態(tài)一致性的最小更新部分放進(jìn)鎖內(nèi)。
例如:
data = await fetch_remote_data()
result = transform(data)
async with lock:
shared_cache[key] = result
當(dāng)然,這個(gè)前提是 fetch_remote_data() 和 transform() 不依賴鎖保護(hù)的不變量。換言之,縮小臨界區(qū)必須在正確性前提下進(jìn)行,而不是機(jī)械追求“鎖內(nèi)代碼越少越好”。
九、常見(jiàn)誤區(qū)二:把所有共享對(duì)象都用一把大鎖保護(hù)
另一種常見(jiàn)錯(cuò)誤是過(guò)度粗粒度加鎖。比如整個(gè)服務(wù)內(nèi)部無(wú)論更新哪個(gè)緩存、哪個(gè)會(huì)話、哪個(gè)連接狀態(tài),都統(tǒng)一使用一把全局鎖。
這種做法的問(wèn)題有兩類(lèi):
- 性能問(wèn)題:原本互不干擾的操作被強(qiáng)行串行化。
- 結(jié)構(gòu)問(wèn)題:鎖的語(yǔ)義邊界變得模糊,后續(xù)維護(hù)者難以判斷每次加鎖究竟是在保護(hù)什么。
更合理的實(shí)踐通常是按資源或不變量進(jìn)行鎖分層,例如:
- 每個(gè)連接對(duì)象持有自己的鎖。
- 每個(gè)用戶會(huì)話維護(hù)自己的鎖。
- 每個(gè)共享映射根據(jù) key 或分片使用不同鎖。
鎖的粒度設(shè)計(jì),本質(zhì)上是在一致性、復(fù)雜度與吞吐之間做權(quán)衡。粒度過(guò)細(xì)會(huì)增加設(shè)計(jì)成本與死鎖風(fēng)險(xiǎn),粒度過(guò)粗則會(huì)嚴(yán)重抑制并發(fā)能力。
十、死鎖風(fēng)險(xiǎn):asyncio并不會(huì)自動(dòng)豁免你
很多人聽(tīng)到“單線程事件循環(huán)”就誤以為不會(huì)死鎖。這是錯(cuò)誤的。死鎖的本質(zhì)不是線程數(shù)量,而是等待關(guān)系是否形成閉環(huán)。
典型風(fēng)險(xiǎn)包括:
- 持有鎖 A 后等待鎖 B,另一個(gè)協(xié)程持有鎖 B 后等待鎖 A。
- 獲取鎖后在某個(gè)不會(huì)返回的等待點(diǎn)長(zhǎng)期掛起,導(dǎo)致其他任務(wù)永遠(yuǎn)無(wú)法進(jìn)入臨界區(qū)。
- 異常路徑遺漏釋放,導(dǎo)致后續(xù)所有等待者永久排隊(duì)。
避免死鎖的幾個(gè)實(shí)用原則是:
- 盡量減少嵌套鎖。
- 如果必須同時(shí)獲取多把鎖,統(tǒng)一全局獲取順序。
- 不要在持鎖狀態(tài)下執(zhí)行邊界不清晰、可能無(wú)限等待的操作。
- 用
async with保證異常路徑釋放。
在復(fù)雜系統(tǒng)中,鎖順序約定比“每個(gè)局部片段看起來(lái)沒(méi)問(wèn)題”更重要。很多死鎖不是單段代碼錯(cuò)誤,而是多段各自合理的代碼組合后形成的系統(tǒng)性問(wèn)題。
十一、asyncio.Lock與其他原語(yǔ)的區(qū)別
正確使用鎖,前提是不要把它當(dāng)成萬(wàn)能工具。
1. 與asyncio.Event的區(qū)別
Event 用來(lái)表達(dá)“某件事是否已經(jīng)發(fā)生”,本質(zhì)上是狀態(tài)通知;它不保證互斥。多個(gè)協(xié)程可以同時(shí)因事件被喚醒并繼續(xù)執(zhí)行。
2. 與asyncio.Semaphore的區(qū)別
Semaphore 允許最多 N 個(gè)協(xié)程同時(shí)進(jìn)入某區(qū)域,適合控制并發(fā)配額,例如限流、連接池許可。鎖則等價(jià)于許可數(shù)為 1 的最簡(jiǎn)單情形,但語(yǔ)義重心是互斥而不是容量控制。
3. 與asyncio.Queue的區(qū)別
Queue 更適合建模生產(chǎn)者-消費(fèi)者數(shù)據(jù)流,把協(xié)程協(xié)調(diào)問(wèn)題轉(zhuǎn)化為消息傳遞問(wèn)題。很多本來(lái)打算用鎖保護(hù)共享列表的場(chǎng)景,實(shí)際上用隊(duì)列會(huì)更清晰、更安全。
4. 與不可變數(shù)據(jù)或單寫(xiě)者模型的區(qū)別
如果系統(tǒng)可以通過(guò)架構(gòu)設(shè)計(jì)避免共享可變狀態(tài),那么往往比“事后加鎖修補(bǔ)”更優(yōu)雅。鎖應(yīng)被視為必要工具,而不是默認(rèn)首選。
十二、什么時(shí)候應(yīng)該用鎖,什么時(shí)候不該用
適合使用 asyncio.Lock 的場(chǎng)景:
- 多個(gè)協(xié)程必須更新同一份可變狀態(tài)。
- 更新動(dòng)作由多個(gè)步驟組成,中間可能發(fā)生
await。 - 需要保證某個(gè)對(duì)象的方法串行執(zhí)行,以維持內(nèi)部狀態(tài)一致。
不適合或應(yīng)謹(jǐn)慎使用的場(chǎng)景:
- 只是為了“讓代碼看起來(lái)更安全”,但并沒(méi)有明確共享狀態(tài)或不變量。
- 試圖用鎖掩蓋阻塞 I/O、CPU 密集計(jì)算等根本問(wèn)題。
- 本質(zhì)上是生產(chǎn)者-消費(fèi)者或通知廣播問(wèn)題,卻誤用鎖做流程編排。
一個(gè)有價(jià)值的判斷標(biāo)準(zhǔn)是:如果拿掉鎖以后,你說(shuō)不清系統(tǒng)會(huì)破壞哪個(gè)具體不變量,那么這把鎖很可能設(shè)計(jì)得并不充分,甚至并不必要。
十三、工程實(shí)踐建議
- 優(yōu)先用
async with lock,除非確實(shí)需要手工控制生命周期。 - 在設(shè)計(jì)鎖之前,先明確要保護(hù)的共享狀態(tài)和不變量。
- 只把必須互斥的最小臨界區(qū)放進(jìn)鎖內(nèi)。
- 避免在持鎖狀態(tài)下執(zhí)行長(zhǎng)時(shí)間 I/O、復(fù)雜回調(diào)或不確定時(shí)長(zhǎng)的等待。
- 盡量減少多把鎖嵌套;若無(wú)法避免,必須約定統(tǒng)一順序。
- 對(duì)高層接口寫(xiě)清楚并發(fā)契約:哪些方法線程安全或協(xié)程安全,哪些調(diào)用方必須自行同步。
- 如果某個(gè)共享資源競(jìng)爭(zhēng)非常激烈,優(yōu)先考慮重構(gòu)為隊(duì)列、分片、局部副本或單寫(xiě)者模型。
十四、結(jié)語(yǔ)
asyncio.Lock 看似只是一個(gè)小型同步原語(yǔ),實(shí)際上它對(duì)應(yīng)的是異步程序中最核心的工程問(wèn)題之一:如何在不犧牲系統(tǒng)一致性的前提下,讓多個(gè)協(xié)程安全地共享狀態(tài)。
它的價(jià)值不在于“把并發(fā)都關(guān)掉”,而在于為那些必須串行化的關(guān)鍵路徑提供清晰、可證明、可維護(hù)的邊界。真正成熟的異步代碼,不是到處機(jī)械加鎖,而是先識(shí)別不變量,再精確劃定臨界區(qū),并在正常路徑、異常路徑、取消路徑上都保持語(yǔ)義完整。
如果把 asyncio 看成一種 I/O 編排模型,那么 asyncio.Lock 的意義就不是“防止多線程搶資源”,而是“在協(xié)作式調(diào)度環(huán)境中,為共享可變狀態(tài)建立秩序”。從這個(gè)角度理解它,才能真正寫(xiě)出既正確又具備工程質(zhì)量的異步程序。
到此這篇關(guān)于深入理解 Python 中的 asyncio.Lock的文章就介紹到這了,更多相關(guān)Python asyncio.Lock內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Python中asyncio.Queue異步隊(duì)列的實(shí)現(xiàn)
- Python異步并發(fā)控制asyncio.gather 與 Semaphore 協(xié)同設(shè)計(jì)解析
- Python?asyncio.run()?和?asyncio.gather()?的區(qū)別和聯(lián)系
- Python異步編程之a(chǎn)syncio.create_task()用法示例解析
- Python異步編程中asyncio.gather的并發(fā)控制詳解
- Python使用asyncio.Queue進(jìn)行任務(wù)調(diào)度的實(shí)現(xiàn)
- Python?Asyncio庫(kù)之a(chǎn)syncio.task常用函數(shù)詳解
- python 使用事件對(duì)象asyncio.Event來(lái)同步協(xié)程的操作
- python中利用隊(duì)列asyncio.Queue進(jìn)行通訊詳解
相關(guān)文章
python利用tkinter實(shí)現(xiàn)圖片格式轉(zhuǎn)換的示例
這篇文章主要介紹了python利用tkinter實(shí)現(xiàn)圖片格式轉(zhuǎn)換,幫助大家更好的理解和使用python,感興趣的朋友可以了解下2020-09-09
詳解Python Socket網(wǎng)絡(luò)編程
這篇文章主要為大家介紹了Python Socket網(wǎng)絡(luò)編程,主要介紹使用 Python 進(jìn)行 TCP Socket網(wǎng)絡(luò)編程2016-01-01
Python字典dict常用方法函數(shù)實(shí)例
這篇文章主要介紹了Python字典dict常用方法函數(shù)實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11
python實(shí)現(xiàn)控制臺(tái)打印的方法
今天小編就為大家分享一篇python實(shí)現(xiàn)控制臺(tái)打印的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
Python實(shí)現(xiàn)非正太分布的異常值檢測(cè)方式
今天小編就為大家分享一篇Python實(shí)現(xiàn)非正太分布的異常值檢測(cè)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-12-12
python+matplotlib實(shí)現(xiàn)鼠標(biāo)移動(dòng)三角形高亮及索引顯示
這篇文章主要介紹了Python+matplotlib實(shí)現(xiàn)鼠標(biāo)移動(dòng)三角形高亮及索引顯示,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
對(duì)Python 兩大環(huán)境管理神器 pyenv 和 virtualenv詳解
今天小編就為大家分享一篇對(duì)Python 兩大環(huán)境管理神器 pyenv 和 virtualenv詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-12-12
Python中列表(List) 的三種遍歷(序號(hào)和值)方法小結(jié)
這篇文章主要介紹了Python中列表(List) 的三種遍歷(序號(hào)和值)方法小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05

