1. 程式人生 > >6.執行緒同步

6.執行緒同步

開發十年,就只剩下這套架構體系了! >>>   

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

為了使它更清楚,假設有兩個或更多執行緒試圖同時在列表中新增物件。 這種行為不能導致成功的結局,因為它會拋棄一個或所有的物件,或者它會完全破壞列表的狀態。 這裡同步的作用是每次只有一個執行緒可以訪問列表。

執行緒同步的問題

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

  • 死鎖
  • 競爭條件

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(500000):  # 數值一定要大
      increment_global()

第5步 - 現在,定義建立執行緒t1

t2main()函式。 兩者都將在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的併發訪問。

E:\Anaconda\python.exe F:/PythonSpace/Concurrent/6.resource_competition.py
x = 869414 after Iteration 0
x = 833985 after Iteration 1
x = 906911 after Iteration 2
x = 903137 after Iteration 3
x = 757507 after Iteration 4

使用鎖的競爭條件

正如我們已經看到上述程式中競爭條件的影響,我們需要一個同步工具,它可以處理多個執行緒之間的競爭條件。 在Python中,threading模組提供了Lock類來處理競爭條件。 此外,Lock類提供了不同的方法,可以通過它幫助處理多個執行緒之間的競爭條件。 

acquire()方法
該方法用於獲取,即阻止鎖定。 鎖可以是阻塞或非阻塞取決於以下真或假的值 -

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

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

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

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

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

import threading

x = 0

def increment_global():
    global x
    x += 1

def task_of_thread(lock):
    for _ in range(500000):  # 數值一定要大
        lock.acquire()
        increment_global()
        lock.release()

def main():
    global x
    x = 0

    lock = threading.Lock()

    t1 = threading.Thread(target=task_of_thread, args=(lock,))
    t2 = threading.Thread(target=task_of_thread, 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))

執行結果:

E:\Anaconda\python.exe F:/PythonSpace/Concurrent/6.resource_competition.py
x = 1000000 after Iteration 0
x = 1000000 after Iteration 1
x = 1000000 after Iteration 2
x = 1000000 after Iteration 3
x = 1000000 after Iteration 4

以下輸出顯示競爭條件的影響被忽略; 在每次迭代之後,x的值現在是100000,這與該程式的期望值相同。

 哲學家就餐問題

死鎖是設計併發系統時可能遇到的麻煩問題。Edsger Dijkstra最初介紹了哲學家就餐問題,這是著名的併發系統最大問題和死鎖問題之一。

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

  • 現在,哲學家有兩個主要條件:首先,每個哲學家既可以進食也可以處於思維狀態;其次,他們必須首先獲得叉子,即左邊和右邊的叉子。
  • 當五位哲學家中的每一位設法同時選擇左叉時,問題就出現了。
  • 他們都在等待右叉是自由的,但他們在未吃完了食物之前永遠不會放棄他們的叉子,並且永遠不會有右叉。

因此,餐桌上會出現死鎖。

併發系統中的死鎖

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

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.Rigthfork = Rightfork

    def print_detail(self):
        print(self.name, self.Leftfork, self.Rigthfork)

    def run(self):
        while self.running:
            time.sleep(random.uniform(3, 10))  # 將隨機生成下一個實數
            print("{} is hungry.".format(self.name))
            self.dine()

    def dine(self):
        fork1, fork2 = self.Leftfork, self.Rigthfork
        while self.running:
            fork1.acquire(True)  # 拿起左手的叉子
            locked = fork2.acquire(False)
            if locked:  # 右邊的叉子有人用,就放下左手的叉子
                fork1.release()
                print("{} puts down fork.".format(self.name))
                time.sleep(3)
            else:  # 右手的叉子沒人用,就拿起來吃
                self.dining()
                try:  # 吃完放下叉子
                    fork2.release()
                    fork1.release()
                except:
                    pass


    def dining(self):
        print("{} starts eating ".format(self.name))
        time.sleep(random.uniform(1, 5))
        print("{} finishes eating and now thinking.".format(self.name))

def Dining_Philosophers():
    forks = [threading.Lock() for _ in range(5)]

    philosopher_names = ["One", "Two", "Three", "Four", "Five"]

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

    DiningPhilosopher.running = True
    for p in philosophers:
        p.start()

    time.sleep(30)
    DiningPhilosopher.running = False

    print("it is finishing")

Dining_Philosophers()

執行結果:

E:\Anaconda\python.exe "F:/PythonSpace/Concurrent/6. dining_philosopher_problem.py"
Five is hungry.
Five puts down fork.
One is hungry.
Two is hungry.
Two puts down fork.
Five starts eating 
Three is hungry.
Four is hungry.
Four starts eating 
Two starts eating 
Four finishes eating and now thinking.
Four puts down fork.
Five finishes eating and now thinking.
Five puts down fork.
Four puts down fork.
Two finishes eating and now thinking.
Two puts down fork.
Four starts eating 
Two starts eating 
Four finishes eating and now thinking.
Four puts down fork.
Two finishes eating and now thinking.
Two puts down fork.
Four starts eating 
Two starts eating 
Two finishes eating and now thinking.
Two puts down fork.
Four finishes eating and now thinking.
Four puts down fork.
Two starts eating 
Two finishes eating and now thinking.
Two puts down fork.
Four starts eating 
it is finishing
Four finishes eating and now thinking.
Five starts eating 
Five finishes eating and now thinking.
One puts down fork.
<