python學習筆記 day39 多執行緒的守護執行緒
1. 守護執行緒
設定子執行緒為守護執行緒,則守護執行緒的程式碼會等待主執行緒程式碼執行完畢而結束:
# 如果列印兩個 子執行緒執行結束,肯定是先列印的守護執行緒的,然後才是子執行緒2的,因為如果子執行緒2先打印出來,那麼主執行緒程式碼就結束了,守護執行緒也就立馬結束,不會在進行列印; # 如果只打印一個 “子執行緒執行結束” 列印的就是子執行緒2的,主執行緒程式碼執行完畢,守護執行緒也結束,來不及列印 from threading import Thread import time def func(): print("子執行緒1開始執行") time.sleep(2) print("子執行緒1執行完畢") t1=Thread(target=func) # 建立一個子執行緒 t1.setDaemon(True) #設定t1子執行緒為守護執行緒,等待主執行緒程式碼執行完畢,子執行緒就結束了 t1.start() # 子執行緒啟動 t2=Thread(target=func) t2.start() t2.join() # 主執行緒等待子執行緒2執行完畢,主執行緒程式碼才算執行完,此時守護執行緒1也會隨著結束
執行結果:
再來看一個例子:
from threading importThread import time def func(): while True: print("子執行緒1開始執行") time.sleep(2) print("子執行緒1執行完畢") t1=Thread(target=func) # 建立一個子執行緒 t1.setDaemon(True) #設定t1子執行緒為守護執行緒,等待主執行緒程式碼執行完畢,子執行緒就結束了 t1.start() # 子執行緒啟動 time.sleep(3) # 主執行緒等三秒,這時候守護執行緒的第一輪迴圈差不多結束,但是第二次只會列印 開始執行,然後主執行緒時間就到了,守護執行緒自然就結束了,不會再列印 執行結束這句話# 其實從守護執行緒的執行函式是一個死迴圈,但是程式執行會正常結束,也可以說明守護執行緒隨著主執行緒程式碼執行結束而結束~
執行結果:
2. GIL全域性直譯器鎖----只是鎖執行緒,並不能真正保證資料安全
GIL只是在執行緒上加鎖,可以保證同一時間只能有一個執行緒操作資料,但是並沒有直接對資料加鎖,所以對某些特殊情況(比如一個執行緒在時間片內操作資料,但程式碼還沒執行完,時間片就輪轉了,接著下一個執行緒操作資料,在時間片內完成了對資料的減一操作,等到時間片輪轉到第一個執行緒時,繼續上次沒執行完的程式碼(上一次把資料取出來放到執行緒暫存器中,但是操作的並不是第二個執行緒減一之後的資料)所以資料雖然被操作了兩次,但是最後仍然是減一放回去了(比如兩個執行緒都是對n減一放回去))資料仍然是不安全的,所以需要再對資料加鎖:
from threading import Thread import time import random def func(): global n # print("這部分程式碼,如果使用lock 開很多個執行緒的話都是多執行緒併發的,只有下面需要對資料加鎖的部分,多執行緒是需要等待,也就是序列的") # time.sleep(3) # print("同樣是上面這段程式碼,如果在每個執行緒開啟後都使用join(),那多個執行緒之間就變為真正的同步了,加鎖,然後在外部統一join()還可以保證上面那段程式碼多執行緒併發") temp = n time.sleep(random.randint(1,3)) # 這三句程式碼,只是為了讓時間片輪轉,一個執行緒執行時在時間片內程式碼並沒有執行完,只是把要操作的資料放到執行緒暫存器, n = temp - 1 # 這樣就會造成資料不安全(可以使用對資料加鎖保證資料安全) n=100 # 程序中的全域性變數,程序中的所有執行緒都可以共享的資料資源 t_lst=[] # 開多個執行緒時放到列表中,最後一起t.join()是為了讓主執行緒需要等待子執行緒執行完畢(因為需要等所有子執行緒都操作完n之後再列印n的值)又能保證多個子執行緒之間併發 for i in range(100): t=Thread(target=func) t.start() t_lst.append(t) [t.join() for t in t_lst] # 所有子執行緒執行結束,才會執行主執行緒列印n的操作(因為本來就是想讓所有子執行緒操作完n 再列印) 否則子執行緒還沒全部執行,主執行緒就執行列印操作了 print("開100個執行緒之後,每個執行緒對程序的全域性變數減一,最終的n值為:%s"%n)
執行結果:
如果對資料進行加鎖呢(就會使資料更加安全)即使時間片內資料沒操作完,但是這個執行緒的鎖還沒釋放,即使下一個執行緒可以操作資料了(GIL的鎖輪到他了),但是沒有拿到資料的鑰匙(對資料加的鎖)因為上一個執行緒佔用了,資料沒執行完,還沒釋放,這樣就可以保證資料的安全性;
from threading import Thread from threading import Lock # 互斥鎖 import time import random def func(): global n # print("這部分程式碼,如果使用lock 開很多個執行緒的話都是多執行緒併發的,只有下面需要對資料加鎖的部分,多執行緒是需要等待,也就是序列的") # time.sleep(3) # print("同樣是上面這段程式碼,如果在每個執行緒開啟後都使用join(),那多個執行緒之間就變為真正的同步了,加鎖,然後在外部統一join()還可以保證上面那段程式碼多執行緒併發") lock.acquire() temp = n time.sleep(0.01) # 這三句程式碼,只是為了讓時間片輪轉,一個執行緒執行時在時間片內程式碼並沒有執行完,只是把要操作的資料放到執行緒暫存器, n = temp - 1 # 這樣就會造成資料不安全(可以使用對資料加鎖保證資料安全) lock.release() n=100 # 程序中的全域性變數,程序中的所有執行緒都可以共享的資料資源 lock=Lock() # 對執行緒需要操作的資料上鎖 t_lst=[] # 開多個執行緒時放到列表中,最後一起t.join()是為了讓主執行緒需要等待子執行緒執行完畢(因為需要等所有子執行緒都操作完n之後再列印n的值)又能保證多個子執行緒之間併發 for i in range(100): t=Thread(target=func) t.start() t_lst.append(t) [t.join() for t in t_lst] # 所有子執行緒執行結束,才會執行主執行緒列印n的操作(因為本來就是想讓所有子執行緒操作完n 再列印) 否則子執行緒還沒全部執行,主執行緒就執行列印操作了 print("開100個執行緒之後,每個執行緒對程序的全域性變數減一,最終的n值為:%s"%n)
執行結果:
可能你會有疑問,那對執行緒的資料加鎖,也就是同一時間只有一個執行緒可以拿到鑰匙,操作資料,其他執行緒都得等著,不又變成同步了,其實這只是對加鎖部分的資料來說,多個執行緒之間確實說序列的,但是對於多個執行緒需要執行的func()函式,不加鎖部分的程式碼,其實多個執行緒是可以併發執行的(可以實現時間複用),如果真的使用join() 那麼所有的程式碼,多個執行緒都是串行同步的了;
3. 死鎖--可以使用遞迴鎖RLock解決
from threading import Thread from threading import Lock import time lock_1=Lock() # 管理麵條的鎖 lock_2=Lock() # 管理筷子的鎖 def func1(name): lock_1.acquire() # 獲取面的鎖 print("%s拿到面了"%name) lock_2.acquire() print("%s 拿到筷子了"%name) print("%s吃麵了"%name) lock_1.release() lock_2.release() def func2(name): lock_2.acquire() print("%s 拿到筷子了"%name) time.sleep(1) # 這裡之所以讓睡一秒,就是想觸發死鎖,讓一個執行緒拿到筷子的鎖,另一個執行緒在該執行緒的等待時間去拿面的鎖,這樣就會出現死鎖,兩個執行緒都進行等待狀態 lock_1.acquire() print("%s拿到面了"%name) print("%s吃麵了"%name) lock_2.release() lock_1.release() Thread(target=func1,args=("xuanxuan",)).start() # 開一個執行緒執行func1函式,會拿面 拿到筷子,吃麵 執行完釋放鎖 Thread(target=func2,args=("xixi",)).start() # 開一個執行緒執行func2函式,先去拿筷子 獲得鎖,然後等1秒 Thread(target=func1,args=("haha",)).start() # 開一個執行緒執行func1 先去拿面,獲得鎖 所以就會出現一種情況,xixi拿著筷子的鎖,haha 拿著面的鎖,兩個都陷入等待狀態 # 上述現象就成為死鎖
執行結果:
如果在不同執行緒中對兩個資料都需要加鎖,一個執行緒拿到其中一把的鑰匙,另一個執行緒拿到另一把,這樣兩個執行緒就都陷入阻塞狀態(死鎖)解決死鎖的方法可以使用遞迴鎖(上面的Lock其實叫互斥鎖,acquire之後必須等到release 下一次的acquire才不會出現阻塞)但是遞迴鎖,可以在一個執行緒中多次acquire 只要最後釋放同等數量release 其他執行緒就可以有機會拿到遞迴鎖的鑰匙:
先看一個簡單版本的:
from threading import Thread from threading import RLock lock=RLock() # 遞迴鎖,一個執行緒只要拿到遞迴鎖,相當於獲取了該執行緒所有需要加鎖資料的萬能鑰匙 def func(): lock.acquire() print("該執行緒中需要加鎖的資料") lock.acquire() # 如果是互斥鎖就不行,必須得等鎖的鑰匙釋放了,才能被別的使用 print("該執行緒中第二個需要加鎖的資料") lock.release() lock.release() Thread(target=func).start()
執行結果:
再來看吃麵的例子---如果使用遞迴鎖就不會出現死鎖問題:
from threading import Thread from threading import RLock import time lock_1=lock_2=RLock() # 多個執行緒中都需要使用兩個需要加鎖的資料,就可以線上程中使用遞迴鎖管理這兩個資料, 遞迴鎖的鑰匙相當於萬能鑰匙,這一個執行緒中可以操作很多需要加鎖的資料 # 這樣一個執行緒拿到遞迴鎖的鑰匙,其他執行緒只能等待,不會出現一個執行緒拿到A資料的鑰匙另一個執行緒拿到B資料的鑰匙的死鎖狀態 def func1(name): lock_1.acquire() # 獲取面的鎖 print("%s拿到面了"%name) lock_2.acquire() print("%s 拿到筷子了"%name) print("%s吃麵了"%name) lock_1.release() lock_2.release() def func2(name): lock_2.acquire() print("%s 拿到筷子了"%name) time.sleep(1) # 這裡之所以讓睡一秒,就是想觸發死鎖,讓一個執行緒拿到筷子的鎖,另一個執行緒在該執行緒的等待時間去拿面的鎖,這樣就會出現死鎖,兩個執行緒都進行等待狀態 lock_1.acquire() print("%s拿到面了"%name) print("%s吃麵了"%name) lock_2.release() lock_1.release() Thread(target=func1,args=("xuanxuan",)).start() # 開一個執行緒執行func1函式,會拿面 拿到筷子,吃麵 執行完釋放鎖 Thread(target=func2,args=("xixi",)).start() # 開一個執行緒執行func2函式,先去拿筷子 獲得鎖,然後等1秒,拿著這把萬能鑰匙 Thread(target=func1,args=("haha",)).start() # 開一個執行緒執行func1 先去拿面,由於xixi開的執行緒已經拿到遞迴鎖的鑰匙,haha在的執行緒就會一直等待,直到xixi執行緒執行完畢,把鎖歸還,haha才有機會拿到鎖 # 所以並不會出現死鎖現象,因為一個執行緒只要第一個鎖爭奪到,就會一直使用,直到完全釋放,其餘執行緒才有機會搶鑰匙
執行結果: