Python 裡的執行緒安全 原子操作

2021-10-09 19:59:54 字數 2818 閱讀 9329

通俗易懂:說說 python 裡的執行緒安全、原子操作

在併發程式設計時,如果多個執行緒訪問同一資源,我們需要保證訪問的時候不會產生衝突,資料修改不會發生錯誤,這就是我們常說的 執行緒安全 。

那什麼情況下,訪問資料時是安全的?什麼情況下,訪問資料是不安全的?如何知道你的**是否執行緒安全?要如何訪問資料才能保證資料的安全?

本篇文章會一一回答你的問題。

執行緒不安全是怎樣的?#

要搞清楚什麼是執行緒安全,就要先了解執行緒不安全是什麼樣的。

比如下面這段**,開啟兩個執行緒,對全域性變數 number 各自增 10萬次,每次自增 1。

copycopy

from threading import thread, lock

number =

0def

target()

:global number

for _ in

range

(1000000):

number +=

1thread_01 = thread(target=target)

thread_02 = thread(target=target)

thread_01.start(

)thread_02.start(

)thread_01.join(

)thread_02.join(

)print

(number)

正常我們的預期輸出結果,乙個執行緒自增100萬,兩個執行緒就自增 200 萬嘛,輸出肯定為 2000000 。

可事實卻並不是你想的那樣,不管你執行多少次,每次輸出的結果都會不一樣,而這些輸出結果都有乙個特點是,都小於 200 萬。

以下是執行三次的結果

copycopy

1459782

1379891

1432921

這種現象就是執行緒不安全,究其根因,其實是我們的操作 number += 1 ,不是原子操作,才會導致的執行緒不安全。

什麼是原子操作?#

原子操作(atomic operation),指不會被執行緒排程機制打斷的操作,這種操作一旦開始,就一直執行到結束,中間不會切換到其他執行緒。

它有點類似資料庫中的 事務。

在 python 的官方文件上,列出了一些常見原子操作

這樣就導致多個執行緒同時讀取時,有可能讀取到同乙個 number 值,讀取兩次,卻只加了一次,最終導致自增的次數小於預期。

當我們還是無法確定我們的**是否具有原子性的時候,可以嘗試通過 dis 模組裡的 dis 函式來檢視

當我們執行這段**時,可以看到 number += 1 這一行**,由兩條位元組碼實現。

binary_add :將兩個值相加

store_global: 將相加後的值重新賦值

每一條位元組碼指令都是乙個整體,無法分割,他實現的效果也就是我們所說的原子操作。

為了對比,我們從上面列表的原子操作拿乙個出來也來試試,是不是真如官網所說的原子操作。

這裡我拿字典的 update 操作舉例,**和執行過程如下圖

從截屏裡可以看到,info.update(new) 雖然也分為好幾個操作

load_global:載入全域性變數

load_attr: 載入屬性,獲取 update 方法

load_fast:載入 new 變數

call_function:呼叫函式

pop_top:執行更新操作

但我們要知道真正會引導資料衝突的,其實不是讀操作,而是寫操作。

上面這麼多位元組碼指令,寫操作都只有乙個(pop_top),因此字典的 update 方法是原子操作。

實現人工原子操作#

在多執行緒下,我們並不能保證我們的**都具有原子性,因此如何讓我們的**變得具有 「原子性」 ,就是一件很重要的事。

方法也很簡單,就是當你在訪問乙個多執行緒間共享的資源時,加鎖可以實現類似原子操作的效果,乙個**要嘛不執行,執行了的話就要執行完畢,才能接受執行緒的排程。

因此,我們使用加鎖的方法,對例子一進行一些修改,使其具備原子性。

copycopy

from threading import thread, lock

number =

0lock = lock(

)def

target()

:global number

for _ in

range

(1000000):

with lock:

number +=

1thread_01 = thread(target=target)

thread_02 = thread(target=target)

thread_01.start(

)thread_02.start(

)thread_01.join(

)thread_02.join(

)print

(number)

此時,不管你執行多少遍,輸出都是 2000000.

為什麼 queue 是執行緒安全的?#

python 的 threading 模組裡的訊息通訊機制主要有如下三種:

event

condition

queue

使用最多的是 queue,而我們都知道它是執行緒安全的。當我們對它進行寫入和提取的操作不會被中斷而導致錯誤,這也是我們在使用佇列時,不需要額外加鎖的原因。

他是如何做到的呢?

其根本原因就是 queue 實現了鎖原語,因此他能像第三節那樣實現人工原子操作。

原語指由若干個機器指令構成的完成某種特定功能的一段程式,具有不可分割性;即原語的執行必須是連續的,在執行過程中不允許被中斷。

執行緒安全性 原子性 atomic

定義 當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式,或者這些程序將如何交替執行,並且在主調 中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麼就稱這個類時安全的 執行緒安全性,主要體現在三個方面,分別是 原子性 提供了互斥訪問,同一時刻只能有乙個執行緒對它進行訪問 可見性 乙...

通過muduo的Atomic h學習原子操作

最新在讀muduo原始碼,想寫乙個原始碼閱讀的系列,就以這個為開篇吧 原始碼如下 templateclass atomicintegert noncopyable uncomment if you need copying and assignment atomicintegert const at...

執行緒安全 互斥鎖和原子變數的效率比較

多執行緒訪問共享資源的時候需要對執行緒進行互斥同步操作,在c 中可以選擇原子變數和c 11中的mutex互斥量物件來進行互斥操作,該文章簡單對兩種同步操作進行乙個效率的比較。工具 vs2017 環境 window7 8核cpu 首先選擇mutex 如下 include include include...