Python多線程編程(二):線程安全與鎖

主要參考文章:https://www.cnblogs.com/ArsenalfanInECNU/p/10022740.html

1.線程安全與鎖

2. Threading.Lock實現互斥鎖(mutex)

3.  死鎖的產生及處理

3.1 迭代死鎖與遞歸鎖(Rlock)

3.1.1 一個線程內部多次加鎖卻沒有釋放 

3.1.2 多個程序間相互調用引起死鎖

3.1.3 遞歸鎖

3.1 互相等待死鎖與鎖的升序使用


1.線程安全與鎖

上一篇文章:https://blog.csdn.net/IanWatson/article/details/104727640說道,當對全局資源存在寫操作時,如果不能保證寫入過程的原子性,會出現髒讀髒寫的情況,即線程不安全。Python的GIL只能保證原子操作的線程安全,而類似加法這種操作不是一種原子操作,因此在多線程編程時我們需要通過加鎖來保證線程安全。

爲了保證函數的原子性操作,線程中引入了鎖的概念。

最簡單的鎖是互斥鎖(同步鎖),互斥鎖是用來解決io密集型場景產生的計算錯誤,即目的是爲了保護共享的數據,同一時間只能有一個線程來修改共享的數據。互斥鎖可以解決多線程中修改count大小這一問題。

2. Threading.Lock實現互斥鎖(mutex)

上一篇文章舉了一個線程不安全的例子:

import threading
count = 0
def run_thread():
    global count
    for i in range(10000):
        count += 1

def run_add():
    t_list = []
    for i in range(10):
        t = threading.Thread(target=run_thread, args=())
        t_list.append(t)
        t.start()
    for t in t_list:
        t.join()
run_add()
print(count)

每個線程使用的字節碼數大於GIL鎖的上限,會導致count的加和失敗。互斥鎖可以解決這個問題。互斥鎖可以保證在使用此線程的時候不被打斷,直至線程中所有的操作都完成。互斥鎖的寫法:

import threading
count = 0
lock = threading.Lock()
def run_thread():
    global count
    if lock.acquire(1):
        # acquire()是獲取鎖,acquire(1)返回獲取鎖的結果,成功獲取到互斥鎖爲True,如果沒有獲取到互斥鎖則返回False
        for i in range(10000):
            count += 1
        lock.release() #用完鎖之後需要釋放

def run_add():
    t_list = []
    for i in range(10):
        t = threading.Thread(target=run_thread, args=())
        t_list.append(t)
        t.start()
    for t in t_list:
        t.join()
run_add()
print(count)

互斥鎖也可以使用with的寫法,和打開文件相似,with使用鎖之後會自動釋放,省去了release的操作:

import threading
count = 0
lock = threading.Lock()
def run_thread():
    global count
    with lock:
        for i in range(10000):
            count += 1

def run_add():
    t_list = []
    for i in range(10):
        t = threading.Thread(target=run_thread, args=())
        t_list.append(t)
        t.start()
    for t in t_list:
        t.join()
run_add()
print(count)

3.  死鎖的產生及處理

3.1 迭代死鎖與遞歸鎖(Rlock)

遞歸鎖介紹:https://www.cnblogs.com/bigberg/p/7910024.html#_label0

標準的鎖對象(threading.Lock)並不關心當前是哪個線程佔有了該鎖;如果該鎖已經被佔有了,那麼任何其它嘗試獲取該鎖的線程都會被阻塞,包括已經佔有該鎖的線程也會被阻塞。於是當一個線程“迭代”請求同一個資源,直接就會造成死鎖。

死鎖是指一個資源被多次調用,而多次調用方都未能釋放該資源就會造成一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖。

下面來那個種情況會導致思索的發生:

3.1.1 一個線程內部多次加鎖卻沒有釋放 

import threading
lock = threading.Lock()
n1 = 0
n2 = 0
def run():
    global n1, n2
    lock.acquire()
    n1 = n1 + 1
    print('set n1 to %s' % str(n1))
    lock.acquire()
    n2 = n2 + 1
    print('set n2 to %s' % str(n2))
    lock.release()
    lock.release()

def run_thread():
    th_list = []
    for i in range(100):
        t = threading.Thread(target=run, args=())
        t.start()
        th_list.append(t)
    for t_each in th_list:
        t_each.join()
run_thread()
print(n1, n2)

輸出爲:

python dead_lock.py
set n1 to 1

然後一直等待

3.1.2 多個程序間相互調用引起死鎖

import threading
num_list = [0, 0]
lock = threading.Lock()
def run_1():
    global num_list
    with lock:
        num_list[0] = num_list[0] + 1
        print('set num_list[0] to %s' % str(num_list[0]))
def run_2():
    global num_list
    with lock:
        num_list[1] = num_list[1] + 1
        print('set num_list[1] to %s' % str(num_list[1]))
def run_3():
    with lock:
        run_1()
        run_2()
        print(num_list)

def run_add():
    t_list = []
    for i in range(10):
        t = threading.Thread(target=run_3, args=())
        t_list.append(t)
        t.start()
    for t in t_list:
        t.join()
run_add()
while threading.active_count() != 1:
    print(threading.active_count())
print(num_list)

示例中,我們有一個共享資源num_list,有兩個函數(run_1和run_2)分別取這個共享資源第一部分和第二部分的數字(num_list[0]和num[1])。兩個訪問函數都使用了鎖來確保在獲取數據時沒有其它線程修改對應的共享數據。
現在,如果我們思考如何添加第三個函數來獲取兩個部分的數據。一個簡單的方法是依次調用這兩個函數,然後返回結合的結果。

這裏的問題是,如有某個線程在兩個函數調用之間修改了共享資源,那麼我們最終會得到不一致的數據。

最明顯的解決方法是在這個函數中也使用lock(run_3)。然而,這是不可行的。裏面的兩個訪問函數將會阻塞,因爲外層語句已經佔有了該鎖

結果是沒有任何輸出,死鎖。

3.1.3 遞歸鎖

解決方法是直接將鎖改爲遞歸鎖RLock()。RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源可以被多次require。直到一個線程所有的acquire都被release,其他的線程才能獲得資源。:

import threading
num_list = [0, 0]
lock = threading.RLock()
def run_1():
    global num_list
    with lock:
        num_list[0] = num_list[0] + 1
        print('set num_list[0] to %s' % str(num_list[0]))
def run_2():
    global num_list
    with lock:
        num_list[1] = num_list[1] + 1
        print('set num_list[1] to %s' % str(num_list[1]))
def run_3():
    with lock:
        run_1()
        run_2()
        print(num_list)

def run_add():
    t_list = []
    for i in range(3):
        t = threading.Thread(target=run_3, args=())
        t_list.append(t)
        t.start()
    for t in t_list:
        t.join()
run_add()
while threading.active_count() != 1:
    print(threading.active_count())
print(num_list)

輸出爲:

python mul_thread.py
set num_list[0] to 1
set num_list[1] to 1
[1, 1]
set num_list[0] to 2
set num_list[1] to 2
[2, 2]
set num_list[0] to 3
set num_list[1] to 3
[3, 3]
[3, 3]

3.1 互相等待死鎖與鎖的升序使用

上下文管理器模塊contextlibhttps://www.cnblogs.com/pyspark/articles/8819803.html

threading的local類:https://www.cnblogs.com/i-honey/p/8051668.html

案例解析:https://www.jb51.net/article/86617.htm

死鎖的另外一個原因是兩個進程想要獲得的鎖已經被對方進程獲得,只能互相等待又無法釋放已經獲得的鎖,而導致死鎖。假設銀行系統中,轉賬函數使用兩個鎖實現,即先扣掉一方的金額,再向另外一方添加金額。用戶a試圖轉賬100塊給用戶b,與此同時用戶b試圖轉賬500塊給用戶a,則可能產生死鎖:

import threading

class Account():
    def __init__(self, name, balance, lock):
        self.name = name
        self.balance = balance
        self.lock = lock
    def withdraw(self, amount):
        self.balance = self.balance - amount
    def deposit(self, amount):
        self.balance = self.balance + amount

def transfer(from_acc, to_acc, amount):
    print("before transfer:%s:%s, %s:%s" % (from_acc.name, from_acc.balance, to_acc.name, to_acc.balance))
    with from_acc.lock:
        from_acc.withdraw(amount)
        print("get lock from %s" % from_acc.name)
        with to_acc.lock:
            to_acc.deposit(amount)
            print("get lock from %s" % to_acc.name)
    print("transfer over")
    print("after transfer:%s:%s, %s:%s" % (from_acc.name, from_acc.balance, to_acc.name, to_acc.balance))

if __name__ == '__main__':
    th_list = []
    acc_a = Account('a', 100, threading.Lock())
    acc_b = Account('b', 100, threading.Lock())
    a_to_b_th = threading.Thread(target=transfer, args=(acc_a, acc_b, 10))
    b_to_a_th = threading.Thread(target=transfer, args=(acc_b, acc_a, 20))
    th_list.append(a_to_b_th)
    th_list.append(b_to_a_th)
    for t in th_list:
        t.start()
    for t in th_list:
        t.join()

輸出爲:

before transfer:a:100, b:100
before transfer:b:100, a:100
get lock from a
get lock from b

2個線程互相等待對方的鎖,互相佔用着資源不釋放,造成死鎖。

即我們的問題是:

多線程程序中,線程需要一次獲取多個鎖,此時如何避免死鎖問題。
解決方案:
多線程程序中,死鎖問題很大一部分是由於線程同時獲取多個鎖造成的。舉個例子:一個線程獲取了第一個鎖,然後在獲取第二個鎖的 時候發生阻塞,那麼這個線程就可能阻塞其他線程的執行,從而導致整個程序假死。 其實解決這個問題,核心思想也特別簡單:目前我們遇到的問題是兩個線程想獲取到的鎖,都被對方線程拿到了,那麼我們只需要保證在這兩個線程中,獲取鎖的順序保持一致就可以了。舉個例子,我們有線程thread_a, thread_b, 鎖lock_1, lock_2。只要我們規定好了鎖的使用順序,比如先用lock_1,再用lock_2,當線程thread_a獲得lock_1時,其他線程如thread_b就無法獲得lock_1這個鎖,也就無法進行下一步操作(獲得lock_2這個鎖),也就不會導致互相等待導致的死鎖。簡言之,解決死鎖問題的一種方案是爲程序中的每一個鎖分配一個唯一的id,然後只允許按照升序規則來使用多個鎖,這個規則使用上下文管理器 是非常容易實現的,示例如下:

# /usr/bin/python3
# -*- coding: utf-8 -*-

import threading
import time
from contextlib import contextmanager

thread_local = threading.local()


@contextmanager
def acquire(*locks):
    # sort locks by object identifier
    locks = sorted(locks, key=lambda x: id(x))

    acquired = getattr(thread_local, 'acquired', [])
    # Acquire all the locks
    acquired.extend(locks)
    thread_local.acquired = acquired

    try:
        for lock in locks:
            lock.acquire()
        yield
    finally:
        for lock in reversed(locks):
            lock.release()
        del acquired[-len(locks):]


class Account(object):
    def __init__(self, name, balance, lock):
        self.name = name
        self.balance = balance
        self.lock = lock

    def withdraw(self, amount):
        self.balance -= amount

    def deposit(self, amount):
        self.balance += amount


def transfer(from_account, to_account, amount):
    print("%s transfer..." % amount)
    with acquire(from_account.lock, to_account.lock):
        from_account.withdraw(amount)
        time.sleep(1)
        to_account.deposit(amount)
    print("%s transfer... %s:%s ,%s: %s" % (
    amount, from_account.name, from_account.balance, to_account.name, to_account.balance))
    print("transfer finish")


if __name__ == "__main__":
    a = Account('a', 1000, threading.Lock())
    b = Account('b', 1000, threading.Lock())
    thread_list = []
    thread_list.append(threading.Thread(target=transfer, args=(a, b, 100)))
    thread_list.append(threading.Thread(target=transfer, args=(b, a, 500)))
    for i in thread_list:
        i.start()
    for j in thread_list:
        j.join()

輸出:

100 transfer...
500 transfer...
100 transfer... a:900 ,b: 1100
transfer finish
500 transfer... b:600 ,a: 1400
transfer finish

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章