執行緒操作之鎖的使用

  • 2019 年 10 月 3 日
  • 筆記

一、執行緒鎖

1.多個執行緒搶佔資源的情況:

鎖通常被用來實現對共享資源的同步訪問。為每一個共享資源創建一個Lock對象,當你需要訪問該資源時,調用acquire方法來獲取鎖對象(如果其它執行緒已經獲得了該鎖,則當前執行緒需等待其被釋放),待資源訪問完後,再調用release方法釋放鎖:

案例一:

from threading import  Thread,Lock  import time  K = Lock()  def func():      global n      K.acquire()       # 加鎖      team = n      time.sleep(0.1)   # 分析:第一個執行緒剛拿到team = n = 100,就遇到阻塞,馬上切換到下一個執行緒,下一個執行緒還是跟上一個執行緒一樣的情況,                        #       由於執行緒切換速度很快,一直拿到的team都是99,除非你的執行緒數量足夠大。                        # 解決方法: 給處理數據部分進行加鎖處理。      n = team-1      K.release()      # 釋放鎖  if __name__ == '__main__':      n = 100      l = []      for i in range(100):          t = Thread(target=func)          l.append(t)          t.start()      for t in l:          t.join()      print(n)  

案例二:

from threading import Thread,Lock  x = 0  K = Lock()  def func():      global x      K.acquire()      for i in range(60000):            x = x+1          # t1 的x剛拿到0 保存狀態 就被切換了          # t2 的x拿到0 進行+1操作  x = 1          # t1 又獲得了運行 ,返回被切之前的保存狀態,x=0+1  x= 1          # 其實運算出來的數字應該是+2,實際上只+1          # 這就產生了數據安全問題      K.release()  if __name__ == '__main__':      t1 = Thread(target=func)      t2 = Thread(target=func)      t3 = Thread(target=func)        t1.start()      t2.start()      t3.start()        t1.join()      t2.join()      t3.join()      print(x)

二、死鎖問題

死鎖: 是指兩個或兩個以上的進程或執行緒在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱為死鎖進程,如下就是死鎖

from threading import Lock as Lock  import time  mutexA=Lock()  mutexA.acquire()  mutexA.acquire()  print(123)  mutexA.release()  mutexA.release()

解決方法,遞歸鎖,在Python中為了支援在同一執行緒中多次請求同一資源,python提供了可重入鎖RLock。

這個RLock內部維護著一個Lock和一個counter變數,counter記錄了acquire的次數,從而使得資源可以被多次require。直到一個執行緒所有的acquire都被release,其他的執行緒才能獲得資源。上面的例子如果使用RLock代替Lock,則不會發生死鎖:

from threading import Thread,RLock,currentThread  import time  k1 = k2 = RLock()   #  一個執行緒拿到鎖,counter加1,該執行緒內又碰到加鎖的情況,則counter繼續加1,這期間所有其他執行緒都只能等待,等待該執行緒釋放所有鎖,即counter遞減到0為止  class Myt(Thread):      def run(self):          self.task1()          self.task2()        def task1(self):          k1.acquire()          print(f'{self.name} 搶到了 鎖1')          k2.acquire()          print(f'{self.name} 搶到了 鎖2')          k2.release()          print(f'{self.name} 釋放了 鎖2')          k1.release()          print(f'{self.name} 釋放了 鎖1')          def task2(self):          k2.acquire()          print(f'{self.name} 搶到了 鎖2')          time.sleep(1)     # 當執行緒1 執行到這一步的時候,遇到阻塞,馬上切換到執行緒2,此時,執行緒1已經拿到了鎖2,但是執行緒2也要拿,所以形成了死鎖。          k1.acquire()      # 解決方法: 加入遞歸鎖RLock          print(f'{self.name} 搶到了 鎖1')          k1.release()          print(f'{self.name} 釋放了 鎖1')          k2.release()          print(f'{self.name} 釋放了 鎖2')    for i in range(3):      t = Myt()      t.start()

三、 訊號量Semaphore

同進程的一樣

Semaphore管理一個內置的計數器,
每當調用acquire()時內置計數器-1;
調用release() 時內置計數器+1;
計數器不能小於0;當計數器為0時,acquire()將阻塞執行緒直到其他執行緒調用release()。

實例:(同時只有5個執行緒可以獲得semaphore,即可以限制最大連接數為5):

from threading import Thread,currentThread,Semaphore    import time  def func():      sm.acquire()      print(f'{currentThread().name} 正在執行')      time.sleep(3)      sm.release()  sm = Semaphore(5)  for i in range(15):      t= Thread(target=func)      t.start()

四、GIL鎖:

GIL:全局解釋器鎖。每個執行緒在執行的過程都需要先獲取GIL,保證同一時刻只有一個執行緒可以執行程式碼。

GIL 與Lock是兩把鎖,本質上也是一把互斥鎖,保護的數據不一樣,前者是解釋器級別的(當然保護的就是解釋器級別的數據,比如垃圾回收的數據),後者是保護用戶自己開發的應用程式的數據,很明顯GIL不負責這件事,只能用戶自定義加鎖處理,即Lock

過程分析:所有執行緒搶的是GIL鎖,或者說所有執行緒搶的是執行許可權

  執行緒1搶到GIL鎖,拿到執行許可權,開始執行,然後加了一把Lock,還沒有執行完畢,即執行緒1還未釋放Lock,有可能執行緒2搶到GIL鎖,開始執行,執行過程中發現Lock還沒有被執行緒1釋放,於是執行緒2進入阻塞,被奪走執行許可權,有可能執行緒1拿到GIL,然後正常執行到釋放Lock。。。這就導致了串列運行的效果

  既然是串列,那我們執行

  t1.start()

  t1.join

  t2.start()

  t2.join()

  這也是串列執行啊,為何還要加Lock呢,需知join是等待t1所有的程式碼執行完,相當於鎖住了t1的所有程式碼,而Lock只是鎖住一部分操作共享數據的程式碼。

五、計算密集型

在 處理像科學計算 這類需要持續使用cpu的類型。

from multiprocessing import Process  from threading import Thread  import os,time    res = 0  def func():      global res      for i in range(10000000):          res += i      if __name__ == '__main__':        print(os.cpu_count())   # CPU核心數      start = time.time()      l = []      for i in range(4):          # t = Process(target=func)          t =Thread(target=func)          l.append(t)          t.start()      for t in l :          t.join()      end = time.time()      print(end-start)   # 多進程耗時:3.4384214878082275   多執行緒耗時:4.417709827423096

由上看出:在處理持續計算長時間使用CPU的程式碼,多進程處理的速度比多執行緒快。推薦使用多進程進行運算。

六、IO密集型

指在處理像IO操作等可能引起阻塞的類型

from multiprocessing import Process  from threading import Thread  import time    def func():      time.sleep(5)    if __name__ == '__main__':      l = []      start = time.time()      for i in range(4):          # t = Process(target=func)  # 多進程          t = Thread(target=func)  # 多執行緒          l.append(t)          t.start()      for t in l:          t.join()      end = time.time()      print(end-start)   # 多進程:5.8953258991241455  多執行緒:5.002920150756836

由上可以看出:在執行IO操作引起阻塞這種任務的時候,多執行緒的速度明顯比多進程要快。

由上面兩個案例,得出結論:

​ python對於計算密集型的任務開多執行緒的效率並不能帶來多大性能上的提升,甚至不如串列(沒有大量切換),但是,對於IO密集型的任務效率還是有顯著提升的。
​
​ 多執行緒用於IO密集型,如socket,爬蟲,web
​ 多進程用於計算密集型,如金融分析

Tags: