同步線程

線程同步可以定義爲一種方法,藉助這種方法,可以確信兩個或更多的併發線程不會同時訪問被稱爲臨界區的程序段。 另一方面,正如我們所知道的那樣,臨界區是共享資源被訪問的程序的一部分。 因此,同步是通過同時訪問資源來確保兩個或更多線程不相互連接的過程。 下圖顯示了四個線程同時嘗試訪問程序的臨界區。

同步線程

爲了使它更清楚,假設有兩個或更多線程試圖同時在列表中添加對象。 這種行爲不能導致成功的結局,因爲它會拋棄一個或所有的對象,或者它會完全破壞列表的狀態。 這裏同步的作用是每次只有一個線程可以訪問列表。

線程同步的問題

在實現併發編程或應用同步基元時,可能會遇到問題。 在本節中,我們將討論兩個主要問題。 問題是 -

  • 死鎖
  • 競爭條件

1. 競爭條件

這是併發編程的主要問題之一。 對共享資源的併發訪問可能會導致競爭狀態。 競爭條件可以定義爲當兩個或更多線程可以訪問共享數據,然後嘗試同時更改其值時發生的條件。 由此,變量的值可能是不可預知的,並且取決於進程的上下文切換的時間而變化。

示例

考慮這個例子來理解競爭條件的概念 -

第1步 - 在這一步中,需要導入線程模塊 -

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步 - 現在,定義創建線程t1t2main()函數。 兩者都將在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 - 如果acquire()方法使用False調用,而False不是默認參數,那麼線程執行不會被阻塞,直到它被設置爲true,即直到它被鎖定。

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

  • 如果鎖定被鎖定,那麼release()方法將解鎖它。 它的工作是如果多個線程被阻塞並且等待鎖被解鎖,則只允許一個線程繼續。
  • 如果鎖已經解鎖,它將引發一個ThreadError錯誤。

現在,我們可以用鎖類及其方法重寫上述程序,以避免競爭條件。 我們需要使用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.