溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么理解Python線程安全

發(fā)布時(shí)間:2021-11-02 15:37:34 來源:億速云 閱讀:132 作者:iii 欄目:編程語言

這篇文章主要介紹“怎么理解Python線程安全”,在日常操作中,相信很多人在怎么理解Python線程安全問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對(duì)大家解答”怎么理解Python線程安全”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!

1. 線程不安全是怎樣的?

要搞清楚什么是線程安全,就要先了解線程不安全是什么樣的。

比如下面這段代碼,開啟兩個(gè)線程,對(duì)全局變量 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)

正常我們的預(yù)期輸出結(jié)果,一個(gè)線程自增100萬,兩個(gè)線程就自增 200 萬嘛,輸出肯定為 2000000 。

可事實(shí)卻并不是你想的那樣,不管你運(yùn)行多少次,每次輸出的結(jié)果都會(huì)不一樣,而這些輸出結(jié)果都有一個(gè)特點(diǎn)是,都小于 200 萬。

以下是執(zhí)行三次的結(jié)果

1459782 1379891 1432921

這種現(xiàn)象就是線程不安全,究其根因,其實(shí)是我們的操作 number += 1 ,不是原子操作,才會(huì)導(dǎo)致的線程不安全。

2. 什么是原子操作?

原子操作(atomic  operation),指不會(huì)被線程調(diào)度機(jī)制打斷的操作,這種操作一旦開始,就一直運(yùn)行到結(jié)束,中間不會(huì)切換到其他線程。

它有點(diǎn)類似數(shù)據(jù)庫中的 事務(wù)。

在 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,其實(shí)等價(jià)于 number = number +  1,可以看到這種可以拆分成多個(gè)步驟(先讀取相加再賦值),并不屬于原子操作。

這樣就導(dǎo)致多個(gè)線程同時(shí)讀取時(shí),有可能讀取到同一個(gè) number 值,讀取兩次,卻只加了一次,最終導(dǎo)致自增的次數(shù)小于預(yù)期。

當(dāng)我們還是無法確定我們的代碼是否具有原子性的時(shí)候,可以嘗試通過 dis 模塊里的 dis 函數(shù)來查看

怎么理解Python線程安全

當(dāng)我們執(zhí)行這段代碼時(shí),可以看到 number += 1 這一行代碼,由兩條字節(jié)碼實(shí)現(xiàn)。

  • BINARY_ADD :將兩個(gè)值相加

  • STORE_GLOBAL:將相加后的值重新賦值

每一條字節(jié)碼指令都是一個(gè)整體,無法分割,他實(shí)現(xiàn)的效果也就是我們所說的原子操作。

當(dāng)一行代碼被分成多條字節(jié)碼指令的時(shí)候,就代表在線程線程切換時(shí),有可能只執(zhí)行了一條字節(jié)碼指令,此時(shí)若這行代碼里有被多個(gè)線程共享的變量或資源時(shí),并且拆分的多條指令里有對(duì)于這個(gè)共享變量的寫操作,就會(huì)發(fā)生數(shù)據(jù)的沖突,導(dǎo)致數(shù)據(jù)的不準(zhǔn)確。

為了對(duì)比,我們從上面列表的原子操作拿一個(gè)出來也來試試,是不是真如官網(wǎng)所說的原子操作。

這里我拿字典的 update 操作舉例,代碼和執(zhí)行過程如下圖

怎么理解Python線程安全

從截圖里可以看到,info.update(new) 雖然也分為好幾個(gè)操作

  • LOAD_GLOBAL:加載全局變量

  • LOAD_ATTR:加載屬性,獲取 update 方法

  • LOAD_FAST:加載 new 變量

  • CALL_FUNCTION:調(diào)用函數(shù)

  • POP_TOP:執(zhí)行更新操作

但我們要知道真正會(huì)引導(dǎo)數(shù)據(jù)沖突的,其實(shí)不是讀操作,而是寫操作。

上面這么多字節(jié)碼指令,寫操作都只有一個(gè)(POP_TOP),因此字典的 update 方法是原子操作。

3. 實(shí)現(xiàn)人工原子操作

在多線程下,我們并不能保證我們的代碼都具有原子性,因此如何讓我們的代碼變得具有 “原子性” ,就是一件很重要的事。

方法也很簡單,就是當(dāng)你在訪問一個(gè)多線程間共享的資源時(shí),加鎖可以實(shí)現(xiàn)類似原子操作的效果,一個(gè)代碼要嘛不執(zhí)行,執(zhí)行了的話就要執(zhí)行完畢,才能接受線程的調(diào)度。

因此,我們使用加鎖的方法,對(duì)例子一進(jìn)行一些修改,使其具備“原子性”。

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)

此時(shí),不管你執(zhí)行多少遍,輸出都是 2000000.

4. 為什么 Queue 是線程安全的?

Python 的 threading 模塊里的消息通信機(jī)制主要有如下三種:

  1. Event

  2. Condition

  3. Queue

使用最多的是  Queue,而我們都知道它是線程安全的。當(dāng)我們對(duì)它進(jìn)行寫入和提取的操作不會(huì)被中斷而導(dǎo)致錯(cuò)誤,這也是我們?cè)谑褂藐?duì)列時(shí),不需要額外加鎖的原因。

他是如何做到的呢?

其根本原因就是 Queue 實(shí)現(xiàn)了鎖原語,因此他能像第三節(jié)那樣實(shí)現(xiàn)人工原子操作。

原語指由若干個(gè)機(jī)器指令構(gòu)成的完成某種特定功能的一段程序,具有不可分割性;即原語的執(zhí)行必須是連續(xù)的,在執(zhí)行過程中不允許被中斷。

到此,關(guān)于“怎么理解Python線程安全”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI