[Python] 信號發生時,context manager 裡的 Lock 未被釋放
前一陣子遇到一個很奇怪的 Python 信號 (signal) 與鎖 (Lock) 的問題,
決定把它記錄一下~
先來看這個簡化過的程式:
import signal import threading import time g_lock = threading.Lock() def signal_handler(signum, frame): log("Inside signal handler") def log(msg): print "Acquring lock..." with g_lock: print "Acquired lock." print "Message:", msg print "Sleep a moment..." time.sleep(10) print "Released lock." signal.signal(signal.SIGUSR1, signal_handler) log("normal")
這個程式註冊了一個 signal handler,
接著呼叫 log() 函式,印出一個訊息。
log() 函式裡面因為某個需求,
必須要用一個鎖 (threading.Lock) 來保護,避免多執行緒的問題。
直接執行這個程式,沒什麼問題,
這個鎖有被成功取得與釋放:
testuser@localhost ~ $ python test_lock.py Acquring lock... Acquired lock. Message: normal Sleep a moment... Released lock.
問題出在 signal handler…
當 log() 函式執行比較慢的時候,
假設這時又接收到了一個 signal,
就會跑去執行 signal handler 的部分 (但並沒有放掉 with g_lock 鎖住的鎖!)
但 signal handler 又再一次呼叫了 log() 函式,
導致它要再拿一次鎖,造成了死鎖 (deadlock)…
要模擬這個狀況,在執行 python 程式後,
趁著 time.sleep() 的時間,執行 kill -USR1 <pid> 將信號送給程式,
就會發現程式停在 Acquring lock… 那一行,再也不會動了:
testuser@localhost ~ $ python test_lock.py Acquring lock... Acquired lock. Message: normal Sleep a moment... Acquring lock...
一個簡單的解決方法,
是改用可重入 (re-entrant) 的鎖,如 threading.RLock(),
這樣同一根執行緒即使已經拿過鎖了,也還是可以再拿第二次~
從下面的執行結果來看,可以看到收到 signal 之後,
在鎖沒放掉的情況下,鎖再次被成功取得。
最後 signal handler 呼叫的 log() 函式放掉一次鎖,
接著回到 signal handler 呼叫前的狀態繼續執行,
把原本 log() 函式執行完,又放掉一次鎖:
testuser@localhost ~ $ python test_lock.py Acquring lock... Acquired lock. Message: normal Sleep a moment... Acquring lock... Acquired lock. Message: Inside signal handler Sleep a moment... Released lock. Released lock.
結論:使用 signal handler 和 Lock 時要很小心,
不要讓裡面執行的東西產生意想不到的副作用啊~