這篇文章主要介紹“怎么理解Python線程安全”,在日常操作中,相信很多人在怎么理解Python線程安全問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么理解Python線程安全”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
1. 線程不安全是怎樣的?
要搞清楚什么是線程安全,就要先了解線程不安全是什么樣的。
比如下面這段代碼,開啟兩個線程,對全局變量 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 萬。
以下是執行三次的結果
1459782 1379891 1432921
這種現象就是線程不安全,究其根因,其實是我們的操作 number += 1 ,不是原子操作,才會導致的線程不安全。
2. 什么是原子操作?
原子操作(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 方法是原子操作。
3. 實現人工原子操作
在多線程下,我們并不能保證我們的代碼都具有原子性,因此如何讓我們的代碼變得具有 “原子性” ,就是一件很重要的事。
方法也很簡單,就是當你在訪問一個多線程間共享的資源時,加鎖可以實現類似原子操作的效果,一個代碼要嘛不執行,執行了的話就要執行完畢,才能接受線程的調度。
因此,我們使用加鎖的方法,對例子一進行一些修改,使其具備“原子性”。
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.
4. 為什么 Queue 是線程安全的?
Python 的 threading 模塊里的消息通信機制主要有如下三種:
Event
Condition
Queue
使用最多的是 Queue,而我們都知道它是線程安全的。當我們對它進行寫入和提取的操作不會被中斷而導致錯誤,這也是我們在使用隊列時,不需要額外加鎖的原因。
他是如何做到的呢?
其根本原因就是 Queue 實現了鎖原語,因此他能像第三節那樣實現人工原子操作。
原語指由若干個機器指令構成的完成某種特定功能的一段程序,具有不可分割性;即原語的執行必須是連續的,在執行過程中不允許被中斷。
到此,關于“怎么理解Python線程安全”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。