通俗易懂:說(shuō)說(shuō) Python 里的線(xiàn)程安全挥萌、原子操作

首發(fā)于微信公眾號(hào):Python編程時(shí)光

在線(xiàn)博客地址:http://python.iswbm.com/en/latest/c01/c01_42.html


在并發(fā)編程時(shí)贼邓,如果多個(gè)線(xiàn)程訪問(wèn)同一資源,我們需要保證訪問(wèn)的時(shí)候不會(huì)產(chǎn)生沖突歹篓,數(shù)據(jù)修改不會(huì)發(fā)生錯(cuò)誤萎战,這就是我們常說(shuō)的 線(xiàn)程安全 咐容。

那什么情況下,訪問(wèn)數(shù)據(jù)時(shí)是安全的蚂维?什么情況下戳粒,訪問(wèn)數(shù)據(jù)是不安全的?如何知道你的代碼是否線(xiàn)程安全虫啥?要如何訪問(wèn)數(shù)據(jù)才能保證數(shù)據(jù)的安全蔚约?

本篇文章會(huì)一一回答你的問(wèn)題。

1. 線(xiàn)程不安全是怎樣的涂籽?

要搞清楚什么是線(xiàn)程安全苹祟,就要先了解線(xiàn)程不安全是什么樣的。

比如下面這段代碼评雌,開(kāi)啟兩個(gè)線(xiàn)程树枫,對(duì)全局變量 number 各自增 10萬(wàn)次,每次自增 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è)線(xiàn)程自增100萬(wàn),兩個(gè)線(xiàn)程就自增 200 萬(wàn)嘛斤吐,輸出肯定為 2000000 搔涝。

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

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

1459782
1379891
1432921

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

2. 什么是原子操作颁独?

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

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

在 Python 的官方文檔上寨辩,列出了一些常見(jiàn)原子操作

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è)線(xiàn)程同時(shí)讀取時(shí),有可能讀取到同一個(gè) number 值甸怕,讀取兩次甘穿,卻只加了一次,最終導(dǎo)致自增的次數(shù)小于預(yù)期梢杭。

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

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

  • BINARY_ADD :將兩個(gè)值相加
  • STORE_GLOBAL: 將相加后的值重新賦值

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

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

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

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

從截圖里可以看到,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í)不是讀操作,而是寫(xiě)操作界斜。

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

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

在多線(xiàn)程下项贺,我們并不能保證我們的代碼都具有原子性君躺,因此如何讓我們的代碼變得具有 “原子性” ,就是一件很重要的事开缎。

方法也很簡(jiǎn)單棕叫,就是當(dāng)你在訪問(wèn)一個(gè)多線(xiàn)程間共享的資源時(shí),加鎖可以實(shí)現(xiàn)類(lèi)似原子操作的效果奕删,一個(gè)代碼要嘛不執(zhí)行俺泣,執(zhí)行了的話(huà)就要執(zhí)行完畢,才能接受線(xiàn)程的調(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 是線(xiàn)程安全的打掘?

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

  1. Event
  2. Condition
  3. Queue

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

他是如何做到的呢横朋?

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

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

關(guān)注公眾號(hào),獲取最新干貨蓖捶!
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末地回,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子俊鱼,更是在濱河造成了極大的恐慌刻像,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件并闲,死亡現(xiàn)場(chǎng)離奇詭異细睡,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)帝火,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)纹冤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)洒宝,“玉大人,你說(shuō)我怎么就攤上這事萌京⊙愀瑁” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵知残,是天一觀的道長(zhǎng)靠瞎。 經(jīng)常有香客問(wèn)我,道長(zhǎng)求妹,這世上最難降的妖魔是什么乏盐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮制恍,結(jié)果婚禮上父能,老公的妹妹穿的比我還像新娘。我一直安慰自己净神,他們只是感情好何吝,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著鹃唯,像睡著了一般爱榕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坡慌,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天黔酥,我揣著相機(jī)與錄音,去河邊找鬼洪橘。 笑死跪者,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的熄求。 我是一名探鬼主播渣玲,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼抡四!你這毒婦竟也來(lái)了柜蜈?” 一聲冷哼從身側(cè)響起仗谆,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤指巡,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后隶垮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體藻雪,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年狸吞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了勉耀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片指煎。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖便斥,靈堂內(nèi)的尸體忽然破棺而出至壤,到底是詐尸還是另有隱情,我是刑警寧澤枢纠,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布像街,位于F島的核電站,受9級(jí)特大地震影響晋渺,放射性物質(zhì)發(fā)生泄漏镰绎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一木西、第九天 我趴在偏房一處隱蔽的房頂上張望畴栖。 院中可真熱鬧,春花似錦八千、人聲如沸吗讶。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)关翎。三九已至,卻和暖如春鸠信,著一層夾襖步出監(jiān)牢的瞬間纵寝,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工星立, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留爽茴,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓绰垂,卻偏偏與公主長(zhǎng)得像室奏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子劲装,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348