為什么需要同步
同樣舉之前的例子,兩個線程分別對同一個全局變量進行加減,得不到預期結果,代碼如下:
total = 0
def add():
global total
for i in range(1000000):
total += 1
def desc():
global total
for i in range(1000000):
total -= 1
import threading
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
原因就是因為 += 和 -=并不是原子操作。可以使用dis模塊查看字節碼:
import dis
def add(total):
total += 1
def desc(total):
total -= 1
total = 0
print(dis.dis(add))
print(dis.dis(desc))
# 運行結果如下:
# 3 0 LOAD_FAST 0 (total)
# 3 LOAD_CONST 1 (1)
# 6 INPLACE_ADD
# 7 STORE_FAST 0 (total)
# 10 LOAD_CONST 0 (None)
# 13 RETURN_VALUE
# None
# 5 0 LOAD_FAST 0 (total)
# 3 LOAD_CONST 1 (1)
# 6 INPLACE_SUBTRACT
# 7 STORE_FAST 0 (total)
# 10 LOAD_CONST 0 (None)
# 13 RETURN_VALUE
# None
可以看到 add()函數雖然其中只有一行代碼,但是字節碼主要分為四個步驟:
load 變量total
load 常量 1
執行加法操作
對total進行賦值
同理,desc()函數的步驟相同,只是第三步改為執行減法。
假設一種極端情況,開始total = 0,首先線程1 load 變量total,得到值為0,切換到線程2,同樣的到total為0,再次切換線程1 load常量1,執行加法,給total賦值得到1;然后線程2也 laod常量1,執行減法,給total賦值為-1。最終total為-1,而不是預期的0。
期望中,必須在+=操作結束后,才能執行-=,所以線程同步的需求就出來了。
互斥鎖Lock
threading模塊中提供了threading.Lock類(互斥鎖),基本用法如下:
import threading
lock = threading.Lock()
lock.acquire() # 獲取鎖
# dosomething…… # 臨界區的代碼只能被同時只能被一個線程運行
lock.release() # 釋放鎖
將上面的代碼修改,即可得到正確結果:
import threading
total = 0
lock = threading.Lock()
def add(lock):
global total
for i in range(1000000):
lock.acquire()
total += 1
lock.release()
def desc(lock):
global total
for i in range(1000000):
lock.acquire()
total -= 1
lock.release()
thread1 = threading.Thread(target=add, args=(lock,))
thread2 = threading.Thread(target=desc, args=(lock,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
# 執行結果 0
可以看到,添加互斥鎖之后,程序執行結果是正確的,但是用了互斥鎖之后,同樣有一些缺陷:
添加鎖之后,會影響程序性能。
可能引起"死鎖"。
死鎖主要在兩種情況下發生:
迭代死鎖
一個線程“迭代”請求同一個資源 ,會造成死鎖。
lock = threading.Lock()
lock.acquire()
lock.acquire()
total += 1
lock.release()
lock.release()
上例種,第一次請求資源后還未 release ,再次acquire,最終無法釋放,造成死鎖 。(可通過可重入鎖解決這個問題)。
互相調用死鎖
兩個線程中都會調用相同的資源,互相等待對方結束的情況 。假設A線程需要資源a,b,B線程也需要資源a,b,而線程A先獲取資源a后,再獲取資源b, B線程先獲取資源b,再獲取資源a。在A線程獲取資源a,B線程獲取資源b后,A線程在等待B線程釋放資源b,而B線程在等待A線程釋放資源a,從而死鎖就發生了。
import threading
import time
lock_a = threading.Lock()
lock_b = threading.Lock()
def func1():
global lock_a
global lock_b
lock_a.acquire()
time.sleep(1)
lock_b.acquire()
time.sleep(1)
lock_b.release()
lock_a.release()
def func2():
global lock_a
global lock_b
lock_b.acquire()
time.sleep(1)
lock_a.acquire()
time.sleep(1)
lock_a.release()
lock_b.release()
thread1 = threading.Thread(target=func1)
thread2 = threading.Thread(target=func2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print("program finished")
# 程序會陷入死循環
這個例子比較重要,開始理解錯了,如果B線程獲取了資源b,然后釋放之后再獲取資源a,這樣是不會發生死鎖的。只有在B線程獲取了資源b,還沒有釋放的時候,獲取了資源a,才會發生死鎖。
可重入鎖RLock
為解決同一線程種不能多次請求同一資源的問題,python提供了“可重入鎖”:threading.RLock,RLock內部維護著一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源可以被多次require。直到一個線程所有的acquire都被release,其他的線程才能獲得資源 。用法和threading.Lock類相同。
將上面迭代死鎖的代碼改寫一下,就不會發生死鎖,但注意,調用acquire和release的次數必須相等。
lock = threading.RLock()
lock.acquire()
lock.acquire()
total += 1
lock.release()
lock.release()
一般不會寫這么無聊的代碼,但是有一種情況是可能發生的,在加鎖區域調用了某個函數,而這個函數內部又申請了同樣的資源。
lock = threading.RLock()
def dosomething(lock):
lock.acquire()
# do something
lock.release()
lock.acquire()
dosomething(lock)
lock.release()
總結
線程間訪問同一變量需要同步。
線程間加鎖會導致性能損失。
加鎖可能產生死鎖,迭代死鎖和互相調用死鎖。
可重入鎖可以避免迭代死鎖。
參考