上一篇:多線程編程
在Python多線程(一):GIL中我們提到了競態(tài)條件問題棠绘,即不同線程修改相同的共享變量出現(xiàn)運(yùn)行多次結(jié)果不一樣的問題嚎杨,即使CPython中有GIL座云,這種問題依然存在。現(xiàn)在我們通過多線程的鎖機(jī)制來解決這個問題旁理。
還是相同的代碼:
import threading
total = 0
def add():
global total
for i in range(1000000):
total += 1
def desc():
global total
for i in range(1000000):
total -= 1
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
之前我們的分析的原因在于:兩加法減法操作在底層實(shí)現(xiàn)的時候有多個步驟樊零,由于GIL的切換導(dǎo)致字節(jié)碼交替運(yùn)行。如果我們能夠保證實(shí)現(xiàn)加法或者減法操作的時候只有一個線程在運(yùn)行孽文,就能解決這個問題驻襟。而保證某一代碼段只有一個線程在運(yùn)行的方法就是為這個線程加鎖夺艰,如下所示:
import threading
total = 0
lock = threading.Lock()
def add():
global total
global lock
for i in range(1000000):
lock.acquire()
total += 1
lock.release()
def desc():
global total
global lock
for i in range(1000000):
lock.acquire()
total -= 1
lock.release()
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
運(yùn)行結(jié)果為0。在上面的代碼中沉衣,threading.Lock()
實(shí)例化了一個鎖對象郁副,鎖對象有兩個方法:acquire
和release
,分別是獲得鎖和釋放鎖厢蒜。當(dāng)一個線程獲得所時霞势,另外一個線程在acquire
處阻塞,直到當(dāng)前鎖執(zhí)行release
被釋放后才可以和其他線程共同爭奪鎖斑鸦。acquire
和release
之間的代碼段執(zhí)行時不會切換到其他線程愕贡,保證了操作的完整性。
用鎖也存在問題巷屿,首先就是性能問題固以,在上面的例子中,不使用鎖運(yùn)行的執(zhí)行時間是0.15秒嘱巾,而使用鎖執(zhí)行時間是2.35秒憨琳,足足慢了15倍。
另外一個問題被稱為死鎖旬昭。當(dāng)一個線程調(diào)用子程序時篙螟,如果這個子程序也需要加鎖,則會出現(xiàn)這個問題:
import threading
import time
lock = threading.Lock()
def do_something():
global lock
lock.acquire()
do_sub_task()
lock.release()
def do_sub_task():
global lock
lock.acquire()
time.sleep(2)
lock.release()
thread = threading.Thread(target=do_something)
thread.start()
thread.join()
程序會在do_sub_task
的首句阻塞问拘,因為該函數(shù)試圖去獲取鎖遍略,但是鎖并沒有釋放。解決方法有兩種:
- 一種是通過
threading.Lock()
再實(shí)例化一把鎖骤坐,使得do_something
和do_sub_task
所需要的鎖不是同一把绪杏,這樣即使do_something
獲取了鎖,do_sub_task
也能夠獲得另外的鎖纽绍。但是這種方式的問題是當(dāng)這種情況出現(xiàn)很多蕾久,鎖就很難管理。 - 另外一種是使用
threading.RLock
拌夏,這種鎖可以重復(fù)獲得僧著,只要釋放的次數(shù)等于獲得的次數(shù)即可。將上面代碼中的Lock
換成RLock
即可障簿。
還有一種死鎖情況稱為互相等待盹愚,參看下面代碼:
import threading
import time
lock1 = threading.Lock()
lock2 = threading.Lock()
def do_something1():
lock1.acquire()
time.sleep(2)
lock2.acquire()
print('Something 1 started')
time.sleep(2)
lock1.release()
lock2.release()
print('Something 1 ended')
def do_something2():
lock2.acquire()
time.sleep(3)
lock1.acquire()
print('Something 2 started')
time.sleep(3)
lock2.release()
lock1.release()
print('Something 2 ended')
thread1 = threading.Thread(target=do_something1)
thread2 = threading.Thread(target=do_something2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
這個程序會一致阻塞,原因在于兩個線程獲得兩個鎖的順序是相反的卷谈,當(dāng)do_something1
運(yùn)行時獲得lock1
,然后執(zhí)行time.sleep(2)
使得GIL釋放去執(zhí)行do_something2
霞篡。do_something2
獲得lock2
后世蔗,同樣執(zhí)行time.sleep(3)
使得GIL釋放去執(zhí)行do_something1
端逼,do_something1
此時需要獲得lock2
才能繼續(xù)執(zhí)行,然而lock2
在do_something2
處污淋,未釋放無法獲得顶滩。同理do_something2
需要獲得的lock1
在do_something1
處,也無法獲得寸爆。所以就出現(xiàn)了兩個線程互相等待的情況礁鲁。如果將其中某個線程獲得的鎖的順序交換,程序就能正常執(zhí)行赁豆。
可以看出仅醇,使用鎖機(jī)制很容易造成死鎖,在使用鎖的時候一定要小心魔种。