執行緒同步



執行緒同步可以定義為一種方法,透過這種方法,我們可以確保兩個或多個併發執行緒不會同時訪問稱為臨界區的程式段。另一方面,眾所周知,臨界區是程式中訪問共享資源的部分。因此,我們可以說同步是確保兩個或多個執行緒不會透過同時訪問資源來相互干擾的過程。下圖顯示了四個執行緒試圖同時訪問程式的臨界區。

Synchronizing

為了更清楚地說明,假設兩個或多個執行緒試圖同時在列表中新增物件。此操作無法成功結束,因為它要麼會丟棄一個或所有物件,要麼會完全破壞列表的狀態。這裡的同步作用是,一次只能有一個執行緒訪問列表。

執行緒同步中的問題

在實現併發程式設計或應用同步原語時,我們可能會遇到問題。在本節中,我們將討論兩個主要問題。這些問題是:

  • 死鎖
  • 競爭條件

競爭條件

這是併發程式設計中的一個主要問題。對共享資源的併發訪問可能導致競爭條件。競爭條件可以定義為當兩個或多個執行緒可以訪問共享資料並嘗試同時更改其值時發生的情況。由於此原因,變數的值可能不可預測,並且會根據程序上下文切換的時機而有所不同。

示例

考慮以下示例以瞭解競爭條件的概念:

步驟 1 - 在此步驟中,我們需要匯入 threading 模組 -

import threading

步驟 2 - 現在,定義一個全域性變數,例如 x,以及其值為 0 -

x = 0

步驟 3 - 現在,我們需要定義 increment_global() 函式,它將在該全域性函式 x 中遞增 1 -

def increment_global():

   global x
   x += 1

步驟 4 - 在此步驟中,我們將定義 taskofThread() 函式,它將呼叫 increment_global() 函式指定次數;對於我們的示例,為 50000 次 -

def taskofThread():

   for _ in range(50000):
      increment_global()

步驟 5 - 現在,定義 main() 函式,其中建立執行緒 t1 和 t2。兩者都將使用 start() 函式啟動,並使用 join() 函式等待它們完成工作。

def main():
   global x
   x = 0
   
   t1 = threading.Thread(target= taskofThread)
   t2 = threading.Thread(target= taskofThread)

   t1.start()
   t2.start()

   t1.join()
   t2.join()

步驟 6 - 現在,我們需要指定範圍,即我們希望呼叫 main() 函式多少次。在這裡,我們呼叫它 5 次。

if __name__ == "__main__":
   for i in range(5):
      main()
      print("x = {1} after Iteration {0}".format(i,x))

在下面顯示的輸出中,我們可以看到競爭條件的影響,因為每次迭代後 x 的值預期為 100000。但是,值存在很大差異。這是由於執行緒對共享全域性變數 x 的併發訪問造成的。

輸出

x = 100000 after Iteration 0
x = 54034 after Iteration 1
x = 80230 after Iteration 2
x = 93602 after Iteration 3
x = 93289 after Iteration 4

使用鎖處理競爭條件

正如我們在上面的程式中看到了競爭條件的影響,我們需要一個同步工具來處理多個執行緒之間的競爭條件。在 Python 中,<threading> 模組提供 Lock 類來處理競爭條件。此外,Lock 類提供了不同的方法,我們可以透過這些方法來處理多個執行緒之間的競爭條件。這些方法描述如下:

acquire() 方法

此方法用於獲取,即阻塞鎖。鎖可以是阻塞的或非阻塞的,具體取決於以下真或假值:

  • 將值設定為 True - 如果使用 True 呼叫 acquire() 方法(這是預設引數),則執行緒執行將被阻塞,直到鎖被解鎖。

  • 將值設定為 False - 如果使用 False 呼叫 acquire() 方法(這不是預設引數),則執行緒執行不會被阻塞,直到它被設定為 true,即直到它被鎖定。

release() 方法

此方法用於釋放鎖。以下是一些與此方法相關的重要的任務:

  • 如果鎖已鎖定,則 release() 方法將解鎖它。它的作用是在多個執行緒被阻塞並等待鎖解鎖時,允許恰好一個執行緒繼續執行。

  • 如果鎖已解鎖,它將引發 ThreadError

現在,我們可以使用 Lock 類及其方法重寫上面的程式,以避免競爭條件。我們需要使用 lock 引數定義 taskofThread() 方法,然後需要使用 acquire() 和 release() 方法來阻塞和非阻塞鎖,以避免競爭條件。

示例

以下是 Python 程式的示例,用於瞭解處理競爭條件的鎖的概念:

import threading

x = 0

def increment_global():

   global x
   x += 1

def taskofThread(lock):

   for _ in range(50000):
      lock.acquire()
      increment_global()
      lock.release()

def main():
   global x
   x = 0

   lock = threading.Lock()
   t1 = threading.Thread(target = taskofThread, args = (lock,))
   t2 = threading.Thread(target = taskofThread, args = (lock,))

   t1.start()
   t2.start()

   t1.join()
   t2.join()

if __name__ == "__main__":
   for i in range(5):
      main()
      print("x = {1} after Iteration {0}".format(i,x))

以下輸出顯示競爭條件的影響被忽略了;因為每次迭代後 x 的值現在都是 100000,這符合該程式的預期。

輸出

x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4

死鎖 - 哲學家就餐問題

死鎖是設計併發系統時可能遇到的一個麻煩問題。我們可以透過哲學家就餐問題來說明這個問題,如下所示:

Edsger Dijkstra 最初提出了哲學家就餐問題,這是併發系統最大問題之一的著名示例,稱為死鎖。

在這個問題中,有五位著名的哲學家圍坐在一張圓桌旁,從他們的碗裡吃一些食物。有五把叉子可以供五位哲學家使用來吃飯。但是,哲學家決定同時使用兩把叉子來吃飯。

現在,哲學家有兩個主要條件。首先,每個哲學家可以處於吃飯狀態或思考狀態,其次,他們必須首先獲得兩把叉子,即左邊和右邊。當所有五位哲學家都設法同時拿起左邊的叉子時,問題就出現了。現在他們都在等待右邊的叉子空閒,但他們永遠不會放棄他們的叉子,直到他們吃完飯,而右邊的叉子永遠不會可用。因此,餐桌上會出現死鎖狀態。

併發系統中的死鎖

現在如果我們看到,同樣的問題也可能出現在我們的併發系統中。上面示例中的叉子將是系統資源,每個哲學家可以表示正在競爭獲取資源的程序。

Python 程式解決方案

這個問題的解決方案可以透過將哲學家分成兩種型別來找到:貪婪哲學家慷慨哲學家。主要是一個貪婪的哲學家會試圖拿起左邊的叉子並等待它在那裡。然後,他將等待右邊的叉子在那裡,拿起它,吃掉它,然後放下它。另一方面,一個慷慨的哲學家會試圖拿起左邊的叉子,如果它不在那裡,他將等待並在一段時間後再次嘗試。如果他們得到左邊的叉子,他們將嘗試得到右邊的叉子。如果他們也得到右邊的叉子,他們將吃飯並釋放兩個叉子。但是,如果他們沒有得到右邊的叉子,他們將釋放左邊的叉子。

示例

以下 Python 程式將幫助我們找到哲學家就餐問題的解決方案:

import threading
import random
import time

class DiningPhilosopher(threading.Thread):

   running = True

   def __init__(self, xname, Leftfork, Rightfork):
   threading.Thread.__init__(self)
   self.name = xname
   self.Leftfork = Leftfork
   self.Rightfork = Rightfork

   def run(self):
   while(self.running):
      time.sleep( random.uniform(3,13))
      print ('%s is hungry.' % self.name)
      self.dine()

   def dine(self):
   fork1, fork2 = self.Leftfork, self.Rightfork

   while self.running:
      fork1.acquire(True)
      locked = fork2.acquire(False)
	  if locked: break
      fork1.release()
      print ('%s swaps forks' % self.name)
      fork1, fork2 = fork2, fork1
   else:
      return

   self.dining()
   fork2.release()
   fork1.release()

   def dining(self):
   print ('%s starts eating '% self.name)
   time.sleep(random.uniform(1,10))
   print ('%s finishes eating and now thinking.' % self.name)

def Dining_Philosophers():
   forks = [threading.Lock() for n in range(5)]
   philosopherNames = ('1st','2nd','3rd','4th', '5th')

   philosophers= [DiningPhilosopher(philosopherNames[i], forks[i%5], forks[(i+1)%5]) \
      for i in range(5)]

   random.seed()
   DiningPhilosopher.running = True
   for p in philosophers: p.start()
   time.sleep(30)
   DiningPhilosopher.running = False
   print (" It is finishing.")

Dining_Philosophers()

上面的程式使用了貪婪和慷慨哲學家的概念。該程式還使用了 <threading> 模組的 Lock 類的 acquire()release() 方法。我們可以在以下輸出中看到解決方案:

輸出

4th is hungry.
4th starts eating
1st is hungry.
1st starts eating
2nd is hungry.
5th is hungry.
3rd is hungry.
1st finishes eating and now thinking.3rd swaps forks
2nd starts eating
4th finishes eating and now thinking.
3rd swaps forks5th starts eating
5th finishes eating and now thinking.
4th is hungry.
4th starts eating
2nd finishes eating and now thinking.
3rd swaps forks
1st is hungry.
1st starts eating
4th finishes eating and now thinking.
3rd starts eating
5th is hungry.
5th swaps forks
1st finishes eating and now thinking.
5th starts eating
2nd is hungry.
2nd swaps forks
4th is hungry.
5th finishes eating and now thinking.
3rd finishes eating and now thinking.
2nd starts eating 4th starts eating
It is finishing.
廣告