python併發編程----對《cookbook》第十二章補充--給關鍵部分加鎖

問題

你需要對多線程程序中的臨界區加鎖以避免競爭條件。

解決方案

要在多線程程序中安全使用可變對象,你需要使用 threading 庫中的 Lock 對象,就像下邊這個例子這樣:

import threading

class SharedCounter:
    '''
    A counter object that can be shared by multiple threads.
    '''
    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._value_lock = threading.Lock()

    def incr(self,delta=1):
        '''
        Increment the counter with locking
        '''
        with self._value_lock:
             self._value += delta

    def decr(self,delta=1):
        '''
        Decrement the counter with locking
        '''
        with self._value_lock:
             self._value -= delta

Lock 對象和 with 語句塊一起使用可以保證互斥執行,就是每次只有一個線程可以執行 with 語句包含的代碼塊。with 語句會在這個代碼塊執行前自動獲取鎖,在執行結束後自動釋放鎖。

討論

線程調度本質上是不確定的,因此,在多線程程序中錯誤地使用鎖機制可能會導致隨機數據損壞或者其他的異常行爲,我們稱之爲競爭條件。爲了避免競爭條件,最好只在臨界區(對臨界資源進行操作的那部分代碼)使用鎖。 在一些“老的” Python 代碼中,顯式獲取和釋放鎖是很常見的。下邊是一個上一個例子的變種:

import threading

class SharedCounter:
    '''
    A counter object that can be shared by multiple threads.
    '''
    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._value_lock = threading.Lock()

    def incr(self,delta=1):
        '''
        Increment the counter with locking
        '''
        self._value_lock.acquire()
        self._value += delta
        self._value_lock.release()

    def decr(self,delta=1):
        '''
        Decrement the counter with locking
        '''
        self._value_lock.acquire()
        self._value -= delta
        self._value_lock.release()

相比於這種顯式調用的方法,with 語句更加優雅,也更不容易出錯,特別是程序員可能會忘記調用 release() 方法或者程序在獲得鎖之後產生異常這兩種情況(使用 with 語句可以保證在這兩種情況下仍能正確釋放鎖)。 爲了避免出現死鎖的情況,使用鎖機制的程序應該設定爲每個線程一次只允許獲取一個鎖。如果不能這樣做的話,你就需要更高級的死鎖避免機制,我們將在12.5節介紹。 在 threading 庫中還提供了其他的同步原語,比如 RLock 和 Semaphore 對象。但是根據以往經驗,這些原語是用於一些特殊的情況,如果你只是需要簡單地對可變對象進行鎖定,那就不應該使用它們。一個 RLock (可重入鎖)可以被同一個線程多次獲取,主要用來實現基於監測對象模式的鎖定和同步。在使用這種鎖的情況下,當鎖被持有時,只有一個線程可以使用完整的函數或者類中的方法。比如,你可以實現一個這樣的 SharedCounter 類:

import threading

class SharedCounter:
    '''
    A counter object that can be shared by multiple threads.
    '''
    _lock = threading.RLock()
    def __init__(self, initial_value = 0):
        self._value = initial_value

    def incr(self,delta=1):
        '''
        Increment the counter with locking
        '''
        with SharedCounter._lock:
            self._value += delta

    def decr(self,delta=1):
        '''
        Decrement the counter with locking
        '''
        with SharedCounter._lock:
             self.incr(-delta)

在上邊這個例子中,沒有對每一個實例中的可變對象加鎖,取而代之的是一個被所有實例共享的類級鎖。這個鎖用來同步類方法,具體來說就是,這個鎖可以保證一次只有一個線程可以調用這個類方法。不過,與一個標準的鎖不同的是,已經持有這個鎖的方法在調用同樣使用這個鎖的方法時,無需再次獲取鎖。比如 decr 方法。 這種實現方式的一個特點是,無論這個類有多少個實例都只用一個鎖。因此在需要大量使用計數器的情況下內存效率更高。不過這樣做也有缺點,就是在程序中使用大量線程並頻繁更新計數器時會有爭用鎖的問題。 信號量對象是一個建立在共享計數器基礎上的同步原語。如果計數器不爲0,with 語句將計數器減1,線程被允許執行。with 語句執行結束後,計數器加1。如果計數器爲0,線程將被阻塞,直到其他線程結束將計數器加1。儘管你可以在程序中像標準鎖一樣使用信號量來做線程同步,但是這種方式並不被推薦,因爲使用信號量爲程序增加的複雜性會影響程序性能。相對於簡單地作爲鎖使用,信號量更適用於那些需要在線程之間引入信號或者限制的程序。比如,你需要限制一段代碼的併發訪問量,你就可以像下面這樣使用信號量完成:

from threading import Semaphore
import urllib.request

# At most, five threads allowed to run at once
_fetch_url_sema = Semaphore(5)

def fetch_url(url):
    with _fetch_url_sema:
        return urllib.request.urlopen(url)

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章