首發於微信公衆號:Python編程時光html
在線博客地址:http://python.iswbm.com/en/latest/c01/c01_42.htmlpython
在併發編程時,若是多個線程訪問同一資源,咱們須要保證訪問的時候不會產生衝突,數據修改不會發生錯誤,這就是咱們常說的 線程安全 。數據庫
那什麼狀況下,訪問數據時是安全的?什麼狀況下,訪問數據是不安全的?如何知道你的代碼是否線程安全?要如何訪問數據才能保證數據的安全?編程
本篇文章會一一回答你的問題。安全
要搞清楚什麼是線程安全,就要先了解線程不安全是什麼樣的。微信
好比下面這段代碼,開啓兩個線程,對全局變量 number 各自增 10萬次,每次自增 1。多線程
from threading import Thread, Lock number = 0 def target(): global number for _ in range(1000000): number += 1 thread_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 萬。app
如下是執行三次的結果函數
1459782 1379891 1432921
這種現象就是線程不安全,究其根因,實際上是咱們的操做 number += 1
,不是原子操做,纔會致使的線程不安全。
原子操做(atomic operation),指不會被線程調度機制打斷的操做,這種操做一旦開始,就一直運行到結束,中間不會切換到其餘線程。
它有點相似數據庫中的 事務。
在 Python 的官方文檔上,列出了一些常見原子操做
L.append(x) L1.extend(L2) x = L[i] x = L.pop() L1[i:j] = L2 L.sort() x = y x.field = y D[x] = y D1.update(D2) D.keys()
而下面這些就不是原子操做
i = i+1 L.append(L[-1]) L[i] = L[j] D[x] = D[x] + 1
像上面的我使用自增操做 number += 1
,其實等價於 number = number + 1
,能夠看到這種能夠拆分紅多個步驟(先讀取相加再賦值),並不屬於原子操做。
這樣就致使多個線程同時讀取時,有可能讀取到同一個 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 方法是原子操做。
在多線程下,咱們並不能保證咱們的代碼都具備原子性,所以如何讓咱們的代碼變得具備 「原子性」 ,就是一件很重要的事。
方法也很簡單,就是當你在訪問一個多線程間共享的資源時,加鎖能夠實現相似原子操做的效果,一個代碼要嘛不執行,執行了的話就要執行完畢,才能接受線程的調度。
所以,咱們使用加鎖的方法,對例子一進行一些修改,使其具有原子性。
from threading import Thread, Lock number = 0 lock = Lock() def target(): global number for _ in range(1000000): with lock: number += 1 thread_01 = Thread(target=target) thread_02 = Thread(target=target) thread_01.start() thread_02.start() thread_01.join() thread_02.join() print(number)
此時,無論你執行多少遍,輸出都是 2000000.
Python 的 threading 模塊裏的消息通訊機制主要有以下三種:
使用最多的是 Queue,而咱們都知道它是線程安全的。當咱們對它進行寫入和提取的操做不會被中斷而致使錯誤,這也是咱們在使用隊列時,不須要額外加鎖的緣由。
他是如何作到的呢?
其根本緣由就是 Queue 實現了鎖原語,所以他能像第三節那樣實現人工原子操做。
原語指由若干個機器指令構成的完成某種特定功能的一段程序,具備不可分割性;即原語的執行必須是連續的,在執行過程當中不容許被中斷。