Python 執行緒安全(同步鎖Lock)詳解

2020-07-16 10:04:45
多執行緒程式設計是一件有趣的事情,它很容易突然出現“錯誤情況”,這是由系統的執行緒排程具有一定的隨機性造成的。不過,即使程式偶然出現問題,那也是由於程式設計不當引起的。當使用多個執行緒來存取同一個資料時,很容易“偶然”出現執行緒安全問題。

執行緒安全問題

關於執行緒安全,有一個經典的“銀行取錢”問題。從銀行取錢的基本流程基本上可以分為如下幾個步驟:
  1. 使用者輸入賬戶、密碼,系統判斷使用者的賬戶、密碼是否匹配。
  2. 使用者輸入取款金額。
  3. 系統判斷賬戶餘額是否大於取款金額。
  4. 如果餘額大於取款金額,則取款成功;如果餘額小於取款金額,則取款失敗。

乍一看上去,這確實就是日常生活中的取款流程,這個流程沒有任何問題。但一旦將這個流程放在多執行緒並行的場景下,就有可能出現問題。注意,此處說的是有可能,並不是說一定。也許你的程式執行了一百萬次都沒有出現問題,但沒有出現問題並不等於沒有問題!

按照上面的流程編寫取款程式,井使用兩個執行緒來模擬模擬兩個人使用同一個賬戶井發取錢操作。此處忽略檢查賬戶和密碼的操作,僅僅模擬後面三步操作。下面先定義一個賬戶類,該賬戶類封裝了賬戶編號和餘額兩個成員變數。
class Account:
    # 定義構造器
    def __init__(self, account_no, balance):
        # 封裝賬戶編號、賬戶餘額的兩個成員變數
        self.account_no = account_no
        self.balance = balance
接下來程式會定義一個模擬取錢的函數,該函數根據執行賬戶、取錢數量進行取錢操作,取錢的邏輯是當賬戶餘額不足時無法提取現金,當餘額足夠時系統吐出鈔票,餘額減少。

程式的主程式非常簡單,僅僅是建立一個賬戶,並啟動兩個執行緒從該賬戶中取錢。程式如下:
import threading
import time
import Account

# 定義一個函數來模擬取錢操作
def draw(account, draw_amount):
    # 賬戶餘額大於取錢數目
    if account.balance >= draw_amount:
        # 吐出鈔票
        print(threading.current_thread().name
            + "取錢成功!吐出鈔票:" + str(draw_amount))
#        time.sleep(0.001)
        # 修改餘額
        account.balance -= draw_amount
        print("t餘額為: " + str(account.balance))
    else:
        print(threading.current_thread().name
            + "取錢失敗!餘額不足!")
# 建立一個賬戶
acct = Account.Account("1234567" , 1000)
# 模擬兩個執行緒對同一個賬戶取錢
threading.Thread(name='甲', target=draw , args=(acct , 800)).start()
threading.Thread(name='乙', target=draw , args=(acct , 800)).start()
先不要管程式中那行被註釋掉的程式碼,上面程式是一個非常簡單的取錢邏輯,這個取錢邏輯與實際的取錢操作也很相似。

多次執行上面程式,很有可能都會看到如圖 1 所示的錯誤結果。

線程安全問題
圖 1 執行緒安全問題