IO阻塞分析:
下面該需求很簡單將一個數值100做自減處到0.主函數中有0.1秒的IO阻塞
import threading
import time
def sub():
global num # 掌握爲什麼加global num
temp=num
time.sleep(0.1)
num=temp-1
time.sleep(2)
num=100
l=[]
for i in range(100):
t=threading.Thread(target=sub,args=())
t.start()
l.append(t)
for t in l: #100個線程的IO阻塞
t.join()
print(num)
"D:\Program Files\python.exe" E:/py_code/多線程/互斥鎖.py
99
Process finished with exit code 0
分析:
通過代碼我們可以看到因爲主函數中增加了一段IO阻塞的代碼,所以我們考慮用到了線程。
1.因爲Python GIL的特性原因,對於多線程並不能真正的實現並行而只能併發
2.當第一個線程搶到了GIL後(temp=100),其他的線程暫時不可以執行,但當第一個線程執行到time.sleep(0.1)時(IO阻塞),此時cup是閒置的,第二個線程可以搶GIL。
3.因爲第一個線程取到temp=100還沒有執行到num=temp-1所以此時第二個線程取到的值還是100,接着也執行到time.sleep(0.1),同理第三個線程搶到GIL。依次類推可以以知道這一百個線程在執行到time.sleep(0.1)之前得到的值都是100,當0.1秒過後全部都依次執行了num=temp-1,所以得出的結果是99.
所以很顯然上面的代碼沒辦法得到我們想要的結果,造成的原因就是IO阻塞和GIL相互作用的原因,所以下面會有另外的途徑去解決
互斥鎖
import threading
import time
def sub():
global num # 掌握爲什麼加global num
lock.acquire() #加鎖直到釋放鎖下個線程纔可以搶鎖
temp=num
time.sleep(0.1)
num=temp-1
lock.release() #釋放鎖下個線程可以搶鎖了
time.sleep(2)
num=100
l=[]
lock=threading.Lock() #生成鎖對象
for i in range(100): #生成100個線程
t=threading.Thread(target=sub,args=())
t.start()
l.append(t)
for t in l: #100個線程的IO阻塞
t.join()
print(num)
"D:\Program Files\python.exe" E:/py_code/多線程/互斥鎖.py
0
Process finished with exit code 0
分析:
1.全局定義一把鎖,因爲進程處理的這個數據num是個公共數據,100個線程進行處理
2.主程序中處理數據的代碼只有下面的這三行:
temp=num
time.sleep(0.1)
num=temp-1
所以將這三行代碼鎖起來,使其在執行完之前其他的線程無法進行干預和取值執行。
3.所以將代碼前加鎖,數據做完運算後釋放鎖。當鎖釋放後其他線程就可以去搶佔這把鎖。
4.線程的運行流程是:
第一個線程搶到GIL,並拿到值temp=100,開始執行代碼,當執行到lock.acquire() 時,下面的代碼加鎖運行,一直執行到time.sleep(0.1),遇到IO阻塞,第二個線程開始搶GIL,搶到後執行代碼執行到lock.acquire(),此時該鎖已經被第一個線程搶佔還未釋放,所以第二個線程只能等第一個線程釋放。
5.當0.1s過後第一個線程繼續執行代碼num=temp-1,此時temp=99,接着執行到lock.release(),鎖釋放第二個線程搶到開始執行temp=num,而此時的temp=99,接着執行到time.sleep(0.1),第三個線程搶佔GIL,但因爲第二個線程未釋放定義鎖,所以無法繼續執行,此時第二個線程執行num=temp-1,此時的num=98.依此類推接下來的線程均依據進行運行。所以最後的執行結果等於0
遞歸鎖
死鎖:
所謂死鎖: 是指兩個或兩個以上的進程或線程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程
import threading
import time
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
self.foo()
self.bar()
def foo(self):
LockA.acquire()
print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
LockB.acquire()
print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
LockB.release()
LockA.release()
def bar(self):
#time.sleep(2)
LockB.acquire()
print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
LockA.acquire()
print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
LockA.release()
LockB.release()
LockA=threading.Lock()
LockB=threading.Lock()
for i in range(10):
t=MyThread()
t.start()
"D:\Program Files\python.exe" E:/py_code/多線程/互斥鎖.py
I am Thread-1 GET LOCKA---->Tue Jul 18 18:46:27 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 18:46:27 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 18:46:27 2017
I am Thread-2 GET LOCKA---->Tue Jul 18 18:46:27 2017
分析:
1.上述的代碼定義了兩把鎖LockA和LockB,並定義一個類,類中繼承線程,則MyThread中就可以調用線程的所有方法。
2.在foo函數中在A鎖中嵌套了B鎖這這裏第一個線程進入執行的邏輯是,執行第一個函數foo,執行第一個打印然後啓動B鎖執行第二個打印釋放B鎖釋放A鎖進入到bar函數,於此同時第二個線程進入到要執行foo,當第一個線程釋放A鎖的那一刻,第二個線程馬上取到A鎖,但是此時第一個線程已經進入到bar函數也同一時刻取到B鎖執行第一個bar函數的第一個打印,當其執行到啓動A鎖時就會卡住,因爲此時的A鎖已經被第二個線程拿到,同理第二個線程此時在foo函數中想啓動B鎖,但是B鎖已經被第一個線程拿到,所以這個時候整個進程就會卡住
3.這是在互斥鎖中常見的問題,所以我們需要引進第二把鎖遞歸鎖:
遞歸鎖
在Python中爲了支持在同一線程中多次請求同一資源,python提供了可重入鎖RLock。這個RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源可以被多次require。直到一個線程所有的acquire都被release,其他的線程才能獲得資源。上面的例子如果使用RLock代替Lock,則不會發生死鎖
import threading
import time
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
self.foo()
self.bar()
def foo(self):
RLock.acquire()
print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
RLock.acquire()
print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
RLock.release()
RLock.release()
def bar(self):
RLock.acquire()
print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
RLock.acquire()
print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
RLock.release()
RLock.release()
RLock=threading.RLock()
for i in range(10):
t=MyThread()
t.start()
"D:\Program Files\python.exe" E:/py_code/多線程/互斥鎖.py
I am Thread-1 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-1 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKA---->Tue Jul 18 19:19:17 2017
Process finished with exit code 0
分析:
1.上述的代碼在全局定義了一把遞歸鎖
2.當第一個線程進入執行時先進入到foo函數,啓動第一把鎖並記錄一次接着執行第一個打印,然後啓動第二把鎖執行第二次打印並釋放兩把鎖。此時包括第一個線程在內這10個線程都能進行搶鎖,這裏RLock採取的就近原則,所以還是第一個線程搶到並進入到bar函數,此時其他線程是搶不到鎖只能繼續等待線程一釋放,線程一順序執行完bar函數並釋放RLock鎖,此刻線程一已經完整的執行完整個工作,並且線程2搶到鎖執行過程和線程一相同。
3.依次類推這10個線程會先後執行完各自的工作。如上述代碼執行的結果所示
信號量
Semaphore管理一個內置的計數器,
每當調用acquire()時內置計數器-1;
調用release() 時內置計數器+1;
計數器不能小於0;當計數器爲0時,acquire()將阻塞線程直到其他線程調用release()
import threading
import time
semaphore=threading.Semaphore(5)
def foo():
semaphore.acquire()
time.sleep(2)
print("ok")
semaphore.release()
for i in range(100):
t=threading.Thread(target=foo,args=())
t.start()
分析:
1.全局定義一個信號量,並限制最大接入的線程數爲5個
2.執行邏輯,當第一個線程進入後semaphore內置計數減1,則這裏可以接收4個線程,此時第一個線程打印第一個OK,接着第二個線程進入打印第二個OK,一直到第五個線程進入後,這時第六個線程在第五個線程沒有執行完semaphore.release()時是不可以進入的,所以按照這個邏輯,該端代碼的執行結果是每5個OK打印一次一共打印100個OK.