一篇文章理清Python多執行緒同步鎖,死鎖和遞迴鎖
python多執行緒的基本使用,大概的內容有幾點
1.建立執行緒物件
t1 = threading.Thread(target=say,args=('tony',))
2.啟動執行緒
t1.start()
後面又說了兩個點就是join和守護執行緒的概念
複製程式碼
但是不知道大家有沒有注意到一點就是前面說的兩個功能是相互獨立的,相互不干涉的,不會用到同享的資源或者資料,如果我們多個執行緒要用到相同的資料,那麼就會存在資源爭用和鎖的問題,不管在什麼語言中,這個都是不能避免的。對資料庫屬性的同學應該也瞭解,資料庫中也存在鎖的概念。
今天這篇文章我們說說python多執行緒中的同步鎖,死鎖和遞迴鎖的使用。
Python同步鎖
鎖通常被用來實現對共享資源的同步訪問。為每一個共享資源建立一個Lock物件,當你需要訪問該資源時,呼叫acquire方法來獲取鎖物件(如果其它執行緒已經獲得了該鎖,則當前執行緒需等待其被釋放),待資源訪問完後,再呼叫release方法釋放鎖。
下面我們來舉個例子說明如果多執行緒在沒有同步鎖的情況下訪問公共資源會導致什麼情況
import threading import time num = 100 def fun_sub(): global num # num -= 1 num2 = num time.sleep(0.001) num = num2-1 if __name__ == '__main__': print('開始測試同步鎖 at %s' % time.ctime()) thread_list = [] for thread in range(100): t = threading.Thread(target=fun_sub) t.start() thread_list.append(t) for t in thread_list: t.join() print('num is %d' % num) print('結束測試同步鎖 at %s' % time.ctime())
上面的例子其實很簡單就是建立100的執行緒,然後每個執行緒去從公共資源num變數去執行減1操作,按照正常情況下面,等到程式碼執行結束,列印num變數,應該得到的是0,因為100個執行緒都去執行了一次減1的操作。
Python學習交流群:556370268,這裡是python學習者聚集地,有大牛答疑,有資源共享!有想學習python程式設計的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入學習。
但是結果卻不是我們想想的,我們看看結果
開始測試同步鎖 at Sun Apr 28 09:56:45 2019
num is 91
結束測試同步鎖 at Sun Apr 28 09:56:45 2019
複製程式碼
我們會發現,每次執行的結果num值都不是一樣的,上面顯示的是91,那就存在問題了,為什麼結果不是0呢?
我們來看看上面程式碼的執行流程。
1.因為GIL,只有一個執行緒(假設執行緒1)拿到了num這個資源,然後把變數賦值給num2,sleep 0.001秒,這時候num=100
2.當第一個執行緒sleep 0.001秒這個期間,這個執行緒會做yield操作,就是把cpu切換給別的執行緒執行(假設執行緒2拿到個GIL,獲得cpu使用權),執行緒2也和執行緒1一樣也拿到num,返回賦值給num2,然sleep,這時候,其實num還是=100.
3.執行緒2 sleep時候,又要yield操作,假設執行緒3拿到num,執行上面的操作,其實num有可能還是100
4.等到後面cpu重新切換給執行緒1,執行緒2,執行緒3上執行的時候,他們執行減1操作後,其實等到的num其實都是99,而不是順序遞減的。
5.其他剩餘的執行緒操作如上
複製程式碼
大家應該發現問題了,結果和我們想想的不一樣,那我們怎麼才能等到我們想要的結果呢?就是100個執行緒操作num變數得到最後結果為0?
這裡就要藉助於python的同步鎖了,也就是同一時間只能放一個執行緒來操作num變數,減1之後,後面的執行緒操作來操作num變數。看看下面我們怎麼實現。
import threading import time num = 100 def fun_sub(): global num lock.acquire() print('----加鎖----') print('現在操作共享資源的執行緒名字是:',t.name) num2 = num time.sleep(0.001) num = num2-1 lock.release() print('----釋放鎖----') if __name__ == '__main__': print('開始測試同步鎖 at %s' % time.ctime()) lock = threading.Lock() #建立一把同步鎖 thread_list = [] for thread in range(100): t = threading.Thread(target=fun_sub) t.start() thread_list.append(t) for t in thread_list: t.join() print('num is %d' % num) print('結束測試同步鎖 at %s' % time.ctime())
看到上面我們給中間的減1程式碼塊,加個一把同步鎖,這樣,我們就可以得到我們想要的結果了,這就是同步鎖的作用,一次只有一個執行緒操作同享資源。
看看上面程式碼執行的結果:
.......
----加鎖----
現在操作共享資源的執行緒名字是: Thread-98
----釋放鎖----
----加鎖----
現在操作共享資源的執行緒名字是: Thread-100
----釋放鎖----
num is 0
結束測試同步鎖 at Sun Apr 28 12:08:27 2019
複製程式碼
Python死鎖
死鎖的這個概念在很多地方都存在,比較在資料中,大概介紹下私有是怎麼產生的 1.A拿了一個蘋果 2.B拿了一個香蕉 3.A現在想再拿個香蕉,就在等待B釋放這個香蕉 4.B同時想要再拿個蘋果,這時候就等待A釋放蘋果 5.這樣就是陷入了僵局,這就是生活中的死鎖
python中線上程間共享多個資源的時候,如果兩個執行緒分別佔有一部分資源並且同時等待對方的資源,就會造成死鎖,因為系統判斷這部分資源都正在使用,所有這兩個執行緒在無外力作用下將一直等待下去。下面是一個死鎖的例子:
import threading import time lock_apple = threading.Lock() lock_banana = threading.Lock() class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.fun1() self.fun2() def fun1(self): lock_apple.acquire() # 如果鎖被佔用,則阻塞在這裡,等待鎖的釋放 print ("執行緒 %s , 想拿: %s--%s" %(self.name, "蘋果",time.ctime())) lock_banana.acquire() print ("執行緒 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime())) lock_banana.release() lock_apple.release() def fun2(self): lock_banana.acquire() print ("執行緒 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime())) time.sleep(0.1) lock_apple.acquire() print ("執行緒 %s , 想拿: %s--%s" %(self.name, "蘋果",time.ctime())) lock_apple.release() lock_banana.release() if __name__ == "__main__": for i in range(0, 10): #建立10個執行緒 my_thread = MyThread() #類繼承法是python多執行緒的另外一種實現方式 my_thread.start() 程式碼執行hung住,死鎖了 執行緒 Thread-1 , 想拿: 蘋果--Sun Apr 28 12:21:06 2019 執行緒 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019 執行緒 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019 執行緒 Thread-2 , 想拿: 蘋果--Sun Apr 28 12:21:06 2019 Process finished with exit code -1
上面的程式碼其實就是描述了蘋果和香蕉的故事。大家可以仔細看看過程。下面我們看看執行流程
1.fun1中,執行緒1先拿了蘋果,然後拿了香蕉,然後釋放香蕉和蘋果,然後再在fun2中又拿了香蕉,sleep 0.1秒。 2.線上程1的執行過程中,執行緒2進入了,因為蘋果被執行緒1釋放了,執行緒2這時候獲得了蘋果,然後想拿香蕉 3.這時候就出現問題了,執行緒一拿完香蕉之後想拿蘋果,返現蘋果被執行緒2拿到了,執行緒2拿到蘋果執行,想拿香蕉,發現香蕉被執行緒1持有了 4.雙向等待,出現死鎖,程式碼執行不下去了
上面就是大概的執行流程和死鎖出現的原因。在這種情況下就是在同一執行緒中多次請求同一資源時候出現的問題。
Python學習交流群:556370268,這裡是python學習者聚集地,有大牛答疑,有資源共享!有想學習python程式設計的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入學習。
Python遞迴鎖RLock
為了支援在同一執行緒中多次請求同一資源,python提供了"遞迴鎖":threading.RLock。RLock內部維護著一個Lock和一個counter變數,counter記錄了acquire的次數,從而使得資源可以被多次acquire。直到一個執行緒所有的acquire都被release,其他的執行緒才能獲得資源。
下面我們用遞迴鎖RLock解決上面的死鎖問題
import threading import time lock = threading.RLock() #遞迴鎖 class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.fun1() self.fun2() def fun1(self): lock.acquire() # 如果鎖被佔用,則阻塞在這裡,等待鎖的釋放 print ("執行緒 %s , 想拿: %s--%s" %(self.name, "蘋果",time.ctime())) lock.acquire() print ("執行緒 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime())) lock.release() lock.release() def fun2(self): lock.acquire() print ("執行緒 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime())) time.sleep(0.1) lock.acquire() print ("執行緒 %s , 想拿: %s--%s" %(self.name, "蘋果",time.ctime())) lock.release() lock.release() if __name__ == "__main__": for i in range(0, 10): #建立10個執行緒 my_thread = MyThread() #類繼承法是python多執行緒的另外一種實現方式 my_thread.start()
上面我們用一把遞迴鎖,就解決了多個同步鎖導致的死鎖問題。大家可以把RLock理解為大鎖中還有小鎖,只有等到內部所有的小鎖,都沒有了,其他的執行緒才能進入這個公共資源。
另外一點前面沒有就算用類繼承的方法實現python多執行緒,這個大家可以查下,就算繼承Thread類,然後重新run方法來實現。
最後大家可能還有個疑問,就算如果我們都加鎖了,也就是單執行緒了,那我們還要開多執行緒有什麼用呢?這裡解釋下,在訪問共享資源的時候,鎖是一定要存在了,但是我們的程式碼中不是總是在訪問公共資源的,還有一些其他的邏輯可以使用多執行緒,所以我們在程式碼裡面加鎖的時候,要注意在什麼地方加,對效能的影響最小,這個就靠對邏輯的理解了。