Python執行緒安全問題及解決方法

gua_niu123發表於2020-12-17

Python多執行緒是通過threading模組來實現的。
一、多執行緒共享全域性變數

from threading import Thread
 
 
list_a = [1, 2, 3]
 
 
def add_list():
    global list_a
    list_a.append(100)
    print(list_a)
 
 
if __name__ == '__main__':
    t1 = Thread(target=add_list)
    t2 = Thread(target=add_list)
    print(t1.name)
    t1.start()
    print(t2.name)
    t2.start()
執行結果:

Thread-1
[1, 2, 3, 100]
Thread-2
[1, 2, 3, 100, 100]

在上面的程式碼中,我們建立了兩個執行緒,這兩個執行緒都是執行一次函式add_list,線上程t1執行完後,全域性變數list_a中多了一個100,線上程t2執行完後,list_a中多了兩個100,說明執行緒t2是線上程t1的基礎上進行新增的。也就是說t1和t2兩個執行緒是共享全域性變數的。

在一個程式內的所有執行緒共享全域性變數,很方便在多個執行緒間共享資料。

但是,多執行緒對全域性變數隨意修改可能造成全域性變數的混亂,產生執行緒安全問題。

二、多執行緒的資源競爭問題(執行緒非安全)

from threading import Thread
 
 
num = 0
 
 
def add_num():
    global num
    for i in range(100000):
        num += 1
 
 
if __name__ == '__main__':
    t1 = Thread(target=add_num)
    t2 = Thread(target=add_num)
    t3 = Thread(target=add_num)
    t1.start()
    t2.start()
    t3.start()
    print(num)
執行結果:

221845

在上面的程式碼中,我們建立了三個執行緒,每個執行緒都是將num進行十萬次+1運算,因為三個執行緒是共享全域性變數的,所以結果應該是三十萬300000。但是,結果卻少了很多(每次執行結果不一樣)。

在多個執行緒對全域性變數進行修改時,造成得到的結果不正確,這種情況就是執行緒安全問題。

如果多個執行緒同時對同一個全域性變數操作,會出現資源競爭問題,從而資料結果會不正確,即遇到執行緒安全問題。

那麼,為什麼多執行緒操作全域性變數時會有資源競爭問題呢?

先假設兩個執行緒t1和t2都要對全域性變數num(從0開始)進行加1運算,t1和t2都各對num加10次,num的最終的結果應該為20。

但是由於是多執行緒同時操作,有可能出現下面情況:

1.在num=0時,t1取得num=0,但還沒有開始做+1運算。此時系統把t1排程為”sleeping”狀態,把t2轉換為”running”狀態,t2也獲得num=0

2.然後t2對得到的值進行加1並賦給num,使得num=1

3.然後系統又把t2排程為”sleeping”,把t1轉為”running”。執行緒t1把它之前得到的0加1後賦值給num。

4.這樣導致雖然t1和t2都對num加1,但結果仍然是num=1

不過,一般在萬級運算次數以下,不會出現資源競爭問題,當上了十萬級或更高量級時,資源競爭問題會越來越明顯。當然,這與電腦的配置也有關。

三、通過同步機制來解決執行緒安全問題

from threading import Thread, Lock, enumerate
import time
 
 
num = 0
mutex = Lock()
 
 
def add_num():
    global num
    for i in range(100000):
        mutex.acquire()
        num += 1
        mutex.release()
 
 
if __name__ == '__main__':
    t1 = Thread(target=add_num)
    t2 = Thread(target=add_num)
    t3 = Thread(target=add_num)
    t1.start()
    t2.start()
    t3.start()
 
    while len(enumerate()) != 1:
        time.sleep(1)
    print(num)
執行結果:

300000

上面的程式碼中,我們給之前的三個執行緒中加了鎖,這樣最後執行的結果就是我們想要的結果。

這種方式是使用互斥鎖來實現同步,避免資源競爭問題發生。

除了使用互斥鎖可以保證執行緒同步外,還有其他方式可以實現同步,解決執行緒安全,如通過佇列來實現同步,因為佇列是序列的,底層封裝了鎖。

四、同步和互斥鎖

同步就是程式按預定的先後次序依次執行。

通過執行緒同步機制,能保證共享資料在任何時刻,最多有一個執行緒訪問,以保證資料的正確性。

注意:

1.執行緒同步就是執行緒排隊

2.共享資源的讀寫才需要同步

3.變數才需要同步,常量不需要同步

當多個執行緒幾乎同時修改某一個共享資料的時候,需要進行同步控制。

執行緒同步能夠保證多個執行緒安全地訪問競爭資源,最簡單的同步機制是使用互斥鎖。

互斥鎖為資源引入了一個狀態:鎖定/非鎖定

某個執行緒要更改共享資料時,先將其鎖定,此時資源的狀態為“鎖定”,其他執行緒不能更改。直到該執行緒釋放資源,將資源的狀態變成“非鎖定”,其他的執行緒才能再次鎖定該資源。互斥鎖保證了每次只有一個執行緒進行操作,從而保證了多執行緒情況下資料的正確性。

注意:

多個執行緒使用的是同一個鎖,如果資料沒有被鎖鎖上,那麼acquire()方法不會堵塞,會執行上鎖操作。

如果在呼叫acquire時,資料已經被其他執行緒上了鎖,那麼acquire()方法會堵塞,直到資料被解鎖為止。

上鎖解鎖過程:

當一個執行緒呼叫鎖的acquire()方法獲得鎖時,鎖就進入“locked”狀態。

每次只有一個執行緒可以獲得鎖。如果此時另一個執行緒試圖獲得這個鎖,該執行緒就會變為“blocked”狀態,稱為“阻塞”,直到擁有鎖的執行緒呼叫鎖的release()方法釋放鎖之後,鎖進入“unlocked”狀態。

執行緒排程程式從處於同步阻塞狀態的執行緒中選擇一個來獲得鎖,並使得該執行緒進入執行(running)狀態。

五、死鎖及解決方法

from threading import Thread, Lock
import time
 
mutex_x = Lock()
mutex_y = Lock()
 
 
def x_func():
    print('X...')
    mutex_x.acquire()
    print('Lock x something')
    time.sleep(1)
    mutex_y.acquire()
    print('Use y something')
    time.sleep(1)
    mutex_y.release()
    print('x...')
    mutex_x.release()
 
 
def y_func():
    print('Y...')
    mutex_y.acquire()
    print('Lock y something')
    time.sleep(1)
    mutex_x.acquire()
    print('Use x something')
    time.sleep(1)
    mutex_x.release()
    print('y...')
    mutex_y.release()
 
 
if __name__ == '__main__':
    t1 = Thread(target=x_func)
    t2 = Thread(target=y_func)
 
    t1.start()
    t2.start()
執行結果:

X...
Y...
Lock x something
Lock y something

上面的程式碼會一直阻塞,程式一直不會結束,因為執行緒1將mutex_x上了鎖,等著鎖mutex_y,與此同時,執行緒2已經將mutex_y上了鎖,等著鎖mutex_x。這樣就形成了死鎖。

線上程間共享多個資源的時候,如果兩個執行緒分別佔有一部分資源並且同時等待對方的資源,就會造成死鎖。

儘管死鎖很少發生,但一旦發生就會造成應用的停止響應。

在程式設計時,要儘量避免死鎖的出現。

為了避免死鎖一直阻塞下去,可以在其中一方新增超時時間,如果超時了,就跳過。

def x_func():
    print('X...')
    mutex_x.acquire()
    print('Lock x something')
    time.sleep(1)
 
    result = mutex_y.acquire(timeout=10)
    print(result)
    print('Use y something')
    time.sleep(1)
    if result:
        mutex_y.release()
 
    print('x...')
    mutex_x.release()
執行結果:

X...
Lock x something
Y...
Lock y something
False
Use y something
x...
Use x something
y...

在上面死鎖的x_func中加入超時時間,則超時後死鎖就解開了。

相關文章