程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

Python 中的多線程設置 (同步)

編輯:Python

文章目錄

  • **線程之間的同步**
  • 線程鎖
  • 多線程處理的一些優點和缺點

本文將講解 Python 編程語言中 多線程處理情況下線程同步的概念。

線程之間的同步

線程同步被定義為一種機制,它確保兩個或多個並發線程不會同時執行某些稱為關鍵段的特定程序段。

關鍵部分是指訪問共享資源的程序部分。

例如,在下圖中,3 個線程嘗試同時訪問共享資源或關鍵部分。

對共享資源的並發訪問可能導致爭用情況

當兩個或多個線程可以訪問共享數據並嘗試同時更改共享數據時,就會發生爭用情況。因此,變量的值可能是不可預測的,並且根據進程的上下文切換的時間而變化。

考慮下面的程序來理解爭用條件的概念:

import threading
# 全局變量 x
x = 0
def increment():
"""用於遞增全局變量x的函數"""
global x
x += 1
def thread_task():
""" 線程的任務調用增量函數100000次。"""
for _ in range(10000000):
increment()
def main_task():
global x
# 將全局變量x設置為0
x = 0
# 創建線程
t1 = threading.Thread(target=thread_task)
t2 = threading.Thread(target=thread_task)
# 開啟線程
t1.start()
t2.start()
# 等待線程完成
t1.join()
t2.join()
if __name__ == "__main__":
for i in range(10):
main_task()
print("迭代 {0}: x = {1}".format(i, x))

運行結果:

在上面的程序中:

  • 在函數main_task中創建兩個線程 t1t2,並且全局變量 x 設置為 0。
  • 每個線程都有一個目標函數thread_task其中增量函數被調用 100000 次。
  • increment 函數將在每次調用中將全局變量 x 遞增 1。

x 的預期最終值為 200000,但我們在函數的 10 次迭代中得到main_task是一些不同的值。

(如果您每次的運行結果都一樣,可能是由於您的計算機性能比較好,可以嘗試這加大thread_task()方法的數據,如多加一個零或幾個零,如10000000次)

發生這種情況是由於線程對共享變量 x 的並發訪問。x 值的這種不可預測性只不過是競態條件

下面給出的是一個圖表,顯示了在上面的程序中如何發生爭用條件

請注意,上圖中 x 的預期值為 12,但由於爭用條件,結果是 11!

因此,我們需要一個工具來在多個線程之間進行適當的同步。

這裡我們就會用到線程鎖了

線程鎖

線程模塊提供了一個 Lock 類來處理爭用條件。鎖定是使用操作系統提供的信號量對象實現的。

信號量是一個同步對象,用於控制多個進程/線程對並行編程環境中公共資源的訪問。它只是操作系統(或內核)存儲中指定位置的值,每個進程/線程都可以檢查該值,然後進行更改。根據找到的值,進程/線程可以使用該資源,或者會發現它已在使用中,並且必須等待一段時間才能重試。信號量可以是二進制(0 或 1),也可以具有其他值。通常,使用信號量的進程/線程會檢查該值,然後,如果它使用資源,則更改該值以反映此值,以便後續信號量用戶將知道等待。

Lock 類提供以下方法:

  • 獲取([阻塞]) : 獲取鎖。鎖可以是阻塞的,也可以是非阻塞的。

    • 當在將阻塞參數設置為 True(默認值)的情況下調用時,線程執行將被阻塞,直到鎖定解鎖,然後鎖定設置為鎖定並返回 True
    • 當在將阻塞參數設置為 False 的情況下調用時,不會阻塞線程執行。如果鎖定已解鎖,則將其設置為鎖定並返回 True, 否則會立即返回 False
  • 釋放() : 釋放鎖。

    • 鎖定後,將其重置為已解鎖,然後返回。如果任何其他線程被阻塞等待鎖定解鎖,請只允許其中一個線程繼續。
    • 如果鎖定已解鎖,則會引發線程錯誤

請考慮下面給出的示例:

import threading
# 全局變量 x
x = 0
def increment():
"""用於遞增全局變量x的函數"""
global x
x += 1
def thread_task(lock):
"""線程的任務調用增量函數100000次."""
for _ in range(100000):
lock.acquire()
increment()
lock.release()
def main_task():
global x
# 設置全局變量為 0
x = 0
# 創建線程鎖
lock = threading.Lock()
# 創建線程
t1 = threading.Thread(target=thread_task, args=(lock,))
t2 = threading.Thread(target=thread_task, args=(lock,))
# 開啟線程
t1.start()
t2.start()
# 等待所有線程完成
t1.join()
t2.join()
if __name__ == "__main__":
for i in range(10):
main_task()
print("迭代 {0}: x = {1}".format(i, x))

運行結果:

讓我們嘗試一步一步地理解上面的代碼:

  • 首先,使用以下命令創建 Lock 對象:

     lock = threading.Lock()
    
  • 然後,將 lock 作為目標函數參數傳遞:

     t1 = threading.Thread(target=thread_task, args=(lock,))
    t2 = threading.Thread(target=thread_task, args=(lock,))
    
  • 在目標函數的關鍵部分,我們使用 lock.acquire() 方法應用 lock。一旦獲得鎖,在使用 lock.release() 方法釋放鎖之前,沒有其他線程可以訪問關鍵部分(此處為增量函數)。

     lock.acquire()
    increment()
    lock.release()
    

    正如您在結果中看到的,x 的最終值每次都顯示為 200000(這是預期的最終結果)。

下面給出了一個圖表,描述了上述程序中鎖的實現:

多線程處理的一些優點和缺點

最後,以下是多線程處理的一些優點和缺點:

優勢:

  • 它不會阻止用戶。這是因為線程彼此獨立。
  • 由於線程並行執行任務,因此可以更好地利用系統資源。
  • 增強了多處理器計算機上的性能。
  • 多線程服務器和交互式 GUI 僅使用多線程處理。

弊:

  • 隨著線程數量的增加,復雜性也會增加。
  • 共享資源(對象、數據)的同步是必要的。
  • 調試難度大,有時結果不可預測。
  • 導致饑餓的潛在死鎖,即某些線程可能無法提供糟糕的設計
  • 構造和同步線程會占用大量 CPU/內存。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved