1. 程式人生 > >python學習筆記 day39 多執行緒的守護執行緒

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 import
Thread 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才有機會拿到鎖
                                                 # 所以並不會出現死鎖現象,因為一個執行緒只要第一個鎖爭奪到,就會一直使用,直到完全釋放,其餘執行緒才有機會搶鑰匙

執行結果: