[Python] 信號發生時,context manager 裡的 Lock 未被釋放

[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 時要很小心,

不要讓裡面執行的東西產生意想不到的副作用啊~

(本頁面已被瀏覽過 62 次)

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。

這個網站採用 Akismet 服務減少垃圾留言。進一步瞭解 Akismet 如何處理網站訪客的留言資料