日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

深入探索并发编程之内存屏障:资源控制操作

發(fā)布時間:2025/3/21 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入探索并发编程之内存屏障:资源控制操作 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

當(dāng)你使用資源控制時, 那么你肯定在試圖理解內(nèi)存執(zhí)行順序。不管你是用C,C++還是其它語言,這都是在編寫無鎖(lock-free)代碼時需要重點考慮的。

在上一篇文章中,我們介紹了編譯期間的內(nèi)存亂序,這一部分內(nèi)容構(gòu)成內(nèi)存執(zhí)行順序問題的一部分。這篇文章講述另一部分:處理器本身在運行期間的內(nèi)存執(zhí)行順序。與編譯器亂序一樣,處理器亂序?qū)τ趩尉€程來說也是不可見的。只有在使用無鎖(lock-free)技術(shù)時-也就是說,當(dāng)共享內(nèi)存在線程之間不是互斥量時,亂序現(xiàn)象才會變得顯露無疑。然而,與編譯器亂序不同的是,?處理器亂序只在多核和多處理器系統(tǒng)中才可見?.

你可以使用任意能充當(dāng)memory barrier的指令來確保執(zhí)行正確的內(nèi)存順序。某種程度上來說,這是你需要了解的唯一技術(shù),因為當(dāng)你使用這類指令時,能自動處理好編譯器執(zhí)行順序問題。充當(dāng)memory barrier的指令包括(但不局限于)以下情況?注1?:

  • GCC中的內(nèi)聯(lián)匯編指令,比如PowerPC平臺下的 asm volatile(“l(fā)wsync” ::: “memory”)
  • 除Xbox 360平臺以外,任意的Win32 Interlocked操作,
  • C++11原子類型操作,比如load(std::memory_order_acquire)
  • POSIX 互斥量操作,比如 pthread_mutex_lock

正因為有許多指令可以充當(dāng)memory barrier,我們需要去了解許多不同類型的memory barrier. 實際上,上述的所有指令產(chǎn)生的memory barrier都屬于不同類型,這會寫lock-free代碼時給你帶來困惑。為了試圖把事情說清楚,我會把那些有助于理解大部分(但不是全部)可能的memory barrier類型做一個類比。

首先,考慮一種典型的多核系統(tǒng)結(jié)構(gòu)。雙核,每個核上有32KiB的L1數(shù)據(jù)緩存,兩個核之間有1MiB的L2共享緩存, 主內(nèi)存512MiB?注2?.

多核系統(tǒng)就有點像是一群程序員通過一種怪異的資源控制策略來合作一個項目。舉個例子,上面的雙核系統(tǒng)對應(yīng)兩個程序員合作的場景。將這兩個程序員分別取名為Larry與Sergey.

右邊有個共享的中心倉庫-代表主內(nèi)存和共享L2緩存的結(jié)合體。Larry和Sergey在本地機器中都有倉庫的工作副本,分別是每個CPU核中的L1緩存。每臺機器上有個臨時存儲區(qū),用來記錄寄存器和本地變量的值。現(xiàn)在兩個程序員坐在那里,富有激情的編輯他們的工作副本和臨時存儲區(qū),根據(jù)他們看見的數(shù)據(jù)決定下一步該做什么–就像是一個線程就在那個CPU核上工作一樣。

在這開始引入資源控制策略。在這個類比里,資源控制策略實際上是非常奇怪的。由于Larry和Sergey修改了倉庫的工作副本,他們的修改在背后不斷地從倉庫中來回傳播(leaking),這種情況發(fā)生的次數(shù)都是隨機的。只要larry編輯文件X,對文件的修改都會泄露到倉庫中,但不能保證什么時候才會發(fā)生。有可能會立即發(fā)生,有可能會發(fā)生很多次,也有可能之后才發(fā)生。這時,他可能會繼續(xù)編輯其它的文件,比如Y和Z,這些改變可能會在X泄露之前就已經(jīng)傳播了。這樣一來,寫操作在寫進(jìn)倉庫的過程中就很容易發(fā)生亂序。

類似的,在Sergey的機器上,不能保證那些修改從倉庫到工作副本中來回傳播的時間間隔和順序。這樣一來,從倉庫讀數(shù)據(jù)的過程中就很容易發(fā)生亂序。

現(xiàn)在,如果每個程序員分別獨自工作在倉庫的隔離區(qū)域,他們都不會意識到背后的傳播過程,甚至不知道另一個程序員的存在。這就好比兩個正在運行的獨立的單線程。在這個例子中,內(nèi)存執(zhí)行順序的基本原則還是能保證的。

當(dāng)程序員開始在倉庫的同一區(qū)域工作時,上述的類比就顯得更加重要了。再來看看之前文章中的例子。 X和Y是全局變量,且都初始化為0.

把X和Y想象成是Larry的工作副本,Sergey的工作副本以及倉庫本身中的文件。Larry將1寫入工作副本中的X,sergey同時將1寫進(jìn)工作副本中的Y。 如果每個程序員在查詢工作副本中的其它文件之前,兩者的修改都能傳回到倉庫中,最后都會得到這個結(jié)果:r1 = 0 和 r2 = 0

。起初可能會覺得這種結(jié)果與直覺上相反,但對照下資源控制的類比方式,就覺得很明顯了。

Memory Barrier類型

幸運的是,Larry和Sergey不完全是對這種隨機性與在背后發(fā)生的不可預(yù)計的傳播無能為力。他們能發(fā)出一些特殊指令,調(diào)用fence指令來充當(dāng)memory barrier). 對于上述類比方式,可以定義四種memory barrier,因此也對應(yīng)四種fence指令。每種memory barrier都是以阻止不同類型的內(nèi)存亂序能力來命名,比如,StoreLoad用來阻止寫讀類型。

就像?Doug?指出的那樣,這四種類型可能并不能很好的對應(yīng)真實CPU中的特殊指令。大部分情況,一條CPU指令能充當(dāng)上述幾種memory barrier類型的組合,可能也會附帶一些其它的效果。不論在什么情況下,只要你以資源控制的類比方式理解了這四種memory barrier,就容易理解真實CPU中的大多數(shù)的指令與一些高級編程語言的構(gòu)造。

LoadLoad

一個LoadLoad barrier能有效地阻止在barrier之前執(zhí)行的讀與在barrier之后執(zhí)行的讀造成的亂序。

在我們的類比中,LoadLoad fence 指令基本等價于倉庫的的pull操作。想象git pull, hg pull, p4 sync, svn update 或者cvs update, 所有操作都在倉庫工作。如果本地的修改有任何的merge沖突,我們就說他們被隨機的決議(resolved)。

要提醒你的是,不能保證LoadLoad會pull整個倉庫的最近(或主干)的修訂版本。只要主干版本至少是從中心倉庫傳播到本地機器的最新值,就能pull比主干版本更老的版本。

這聽起來可能像一個較弱的保證,但仍然是一個完美的方案來阻止讀取過時的數(shù)據(jù)。考慮一個經(jīng)典的例子,Sergey檢查共享flag來看Larry是不是發(fā)布了一些數(shù)據(jù)。如果flag為真,在讀取發(fā)布的數(shù)據(jù)之前,Sergey發(fā)出一個LoadLoad barrier 。

if (IsPublished) // Load and check shared flag {LOADLOAD_FENCE(); // Prevent reordering of loadsreturn Value; // Load published value } Load and check shared flag {LOADLOAD_FENCE(); // Prevent reordering of loadsreturn Value; // Load published value }

顯然,這個例子依賴于IsPublished標(biāo)志位是否傳播到了Sergey的工作副本。不用去關(guān)心這些是什么時候發(fā)生的。只要發(fā)現(xiàn)了傳播的flag,它就發(fā)出一個LOadLoad fence來阻止讀取Value的值(這個值比flag本身還要老).

StoreStore

一個StoreStore barrier能有效的阻止在barrier之前的寫操作與在barrier之后的寫操作之間的亂序。

在我們的類比中,StoreStore fence指令對應(yīng)倉庫的push操作。想象git push, hg push, p4 submit, svn commit or cvs commit 都發(fā)生在整個倉庫中。

跟繞口令一樣,假設(shè)StoreStore指令不是即時的,而是以異步的方式延后執(zhí)行。因此,盡管Larry執(zhí)行了StoreStore指令,我們對于他之前所有的寫操作什么時候能再在倉庫中出現(xiàn)不能做任何的假設(shè)。

這聽起來可能也像是弱的保證,但是,已經(jīng)足夠來阻止Sergey收到Larry發(fā)布的任何過時的數(shù)據(jù)。 回到上面同樣的例子, 這時Larry只需要發(fā)布一些數(shù)據(jù)到共享內(nèi)存,發(fā)出一個 StoreStore barrier,然后將共享flag設(shè)置為true

Value = x; // Publish some data STORESTORE_FENCE(); IsPublished = 1; // Set shared flag to indicate availability of data Set shared flag to indicate availability of data

再說一次,我們依賴從Larry的工作副本中傳播到Sergey的Ispublished值. 一旦Sergey檢測到,他相信自己看到了Value的正確值。有趣的是,在這種工作模式中,Value不用是原子類型,也可以是有許多元素的大結(jié)構(gòu)體。

LoadStore

不像LoadLoad和StoreStore,就資源控制操作來看,LoadStore沒有比較合適的類比。 理解LoadStore的最好方法很簡單,就是考慮指令亂序。

想象Larry有一系列指令要執(zhí)行。某些指令讓他從自己的工作副本讀取數(shù)據(jù)到寄存器中,以及某些指令從寄存器中寫數(shù)據(jù)到工作副本中。Larry具備欺騙指令的能力,但只限于一些特殊場合。只要他遇到讀操作時,他就會先檢測讀操作之后的任何寫操作。如果寫操作和當(dāng)前的讀操作完全不相關(guān),他會先略過讀操作,先進(jìn)行寫操作,結(jié)束后在進(jìn)行讀操作。在這種場景下,內(nèi)存執(zhí)行順序的基本原則–絕不修改單程序的行為–仍然是遵守了的。

對于真實CPU,如果某些處理器中有一個緩存錯過了讀操作(緊接著緩存命中的寫操作),這種指令亂序就可能會發(fā)生。為了理解這個類比,硬件細(xì)節(jié)并不重要。我們只當(dāng)做Larry的工作很繁瑣,而這正是他能創(chuàng)新的機會(這種機會很有限)。 不管他是否選擇這么做都是完全不可預(yù)計的。幸運的是,阻止這種亂序類型的開銷并不大。當(dāng)Larry遇到一個LoadStore barrier, 他就能簡單的避免barrier附近的亂序。

在我們的類比中,盡管在讀寫之間有個LoadLoad或者StoreStore barrier時,Larry執(zhí)行這種類型的LoadStore亂序也是有效的。 然而,在真實的CPU中,充當(dāng)LoadStore barrier的指令至少能充當(dāng)其它兩種類型的barrier。

StoreLoad

StoreLoad barrier能確保其它處理器遇到barrier之前執(zhí)行所有的寫操作,另外,barrier之后執(zhí)行的所有讀操作能收到最近能被barrier可見的值。 .換句話說,它能在barrier處理所有的讀操作之前有效地阻止所有的寫操作亂序,尊重順序一致性多處理器執(zhí)行那些操作的方式。

StoreLoad是唯一的。這是唯一的一種memory barrier類型可以阻止前文提到的這種結(jié)果:r1 = r2 = 0,這個例子我在前面的文章中提到過很多次了。

如果你一路仔細(xì)地讀下來,可能會有個疑惑: StoreLoad和 StoreStore再緊接著一個LoadLoad有什么不一樣呢?畢竟,StoreStore 將修改push到倉庫中,然而LoadLoad 把遠(yuǎn)程的修改pull回來。然而,那兩種barrier類型是不夠的。記住,push操作可能會因為任意數(shù)量的指令而延遲, pull操作可能不會從主干版本中pull數(shù)據(jù)。 這也解釋了為什么owerPC的lwsync指令–充當(dāng)所有LoadLoad, LoadStore和StoreStore memory barrier,但不是StoreLoad–是不足夠來阻止例子中的r1 = r2 = 0 這種結(jié)果的。

拿類比來說,StoreLoad barrier能通過push所有局部修改到倉庫中來實現(xiàn),等待那個操作完成,然后pull 倉庫中絕對的、最近的主干修訂版本。在大部分處理器上,充當(dāng)StoreLoad barrier的指令比充當(dāng)其它類型barrier的指令開銷更大。

如果在那個操作中放置一個LoadStore barrier,也沒什么大不了的, 之后我們得到的是一個完整的memory fence–一次性充當(dāng)所有四種barrier 類型。 正如?Doug?指出的那樣,在目前所有處理器上都是這樣,每個充當(dāng)StoreLoad barrier的指令也充當(dāng)完整的memory fence.

類比給你帶來了什么?

正如我之前提到的那樣,在處理內(nèi)存執(zhí)行順序時,每個處理器都有不同的?特點?。具體來說,在x86/64家族中有一種強內(nèi)存模型。這是為了讓內(nèi)存亂序的發(fā)生降到最低。PowerPC和ARM有著弱的內(nèi)存模型。Alpha因自成一派而出名。幸運的是,這篇文章的類比對應(yīng)一種弱的?內(nèi)存模型?。 如果你能用心對待它,并使用這里提供的fence指令來實施正確的內(nèi)存執(zhí)行順序,你就能處理大部分的CPU。

這種類比同樣對應(yīng)針對C++11和C11的抽象機器。因此,如果你使用那些語言的標(biāo)準(zhǔn)庫寫無鎖(lock-free)代碼,同時將上述的類比記在腦里,就更有可能在任何平臺下正確執(zhí)行。

在這種類比中,我說過,每個程序員代表在一個核心中正在運行的單線程。在一個真實的操作系統(tǒng)中,線程更傾向在生命周期中在不同的核心里移動,這時上述的類比仍然有效。我也在機器語言和C/C++語言不斷變換來舉例。顯然,我們更傾向使用C/C++,或者另外更高級的語言。這是有可能的,因為任何充當(dāng)memory barrier的操作也能阻止編譯器亂序。

我還沒有寫關(guān)于每種memory barrier類型的文章。例如,也存在數(shù)據(jù)依賴(data dependency )barriers?注3?。 我會在以后的文章中講這些內(nèi)容。然而,上面給出的四種類型仍是最重要的。

如果你對CPU在底層是如何工作的很感興趣–像寫緩存,緩存一致性協(xié)議(cache coherency protocol)以及硬件實施細(xì)節(jié)相關(guān)的東西–以及為什么它們?yōu)槭裁磿l(fā)生內(nèi)存亂序注4?,我推薦Paul Paul McKenney & David Howell的?工作?。 實際上,我認(rèn)為能成功寫好無鎖(lock-free)代碼的大部分程序員都至少都對這種硬件細(xì)節(jié)有點熟悉。

注釋

注1:gcc+x86下,編譯器級別的memory barrier和CPU級別的memory barrier可以如下實現(xiàn):

define COMPILER_BARRIER() __asm__ __volatile__("" : : : "memory") define CPU_BARRIER() __sync_synchronize COMPILER_BARRIER() __asm__ __volatile__("" : : : "memory") define CPU_BARRIER() __sync_synchronize

其中,?CPU_BARRIER()?可以防止CPU寫讀、寫寫、讀寫、讀讀亂序。如你所知,CPU級別的memory barrier同時約束CPU和編譯器的亂序;而編譯器級別的memory barrier只約束編譯器的亂序,不影響CPU。

注2:現(xiàn)在用的很多的cpu一般有三級cache。

注3:為了演示data dependency barrier,考慮以下例子:

初始化 int A = 1; int B = 2; int C = 3; int *P = &A; int *Q = &B; //cpu1 B = 4; CPU_BARRIER(); P = &B; //cpu2 Q = P; D = *Q int A = 1; int B = 2; int C = 3; int *P = &A; int *Q = &B; //cpu1 B = 4; CPU_BARRIER(); P = &B; //cpu2 Q = P; D = *Q

從直覺上說,Q最后要么等于&A,要么等于&B。也就是說:

Q == &A, D == 1

或者

Q == &B, D == 4

但是,讓人吃驚的是,在某些CPU體系結(jié)構(gòu)例如DEC Alpha下,可能出現(xiàn)

Q == &B, D == 2的情形,也就是說執(zhí)行順序(total order,global order)是:

D = *Q B = 4; P = &B; Q = P; = *Q B = 4; P = &B; Q = P;

也就是說CPU2的兩行賦值發(fā)生了亂序,而我們知道,這里是存在數(shù)據(jù)依賴的,順序非常關(guān)鍵。因此,這里需要加一個data dependency barrier。

注4:為什么CPU亂序只在多核多線程下才可能會暴露出問題?為什么X86體系結(jié)構(gòu)的Intel CPU要對寫讀進(jìn)行亂序?

要明白這兩個問題,我們首先得知道cache coherency,也就是所謂的cache一致性。

在現(xiàn)代計算機里,一般包含至少三種角色:cpu、cache、內(nèi)存。一般說來,內(nèi)存只有一個;CPU Core有多個;cache有多級,cache的基本塊單位是cacheline,大小一般是64B-256B。

每個cpu core有自己的私有的cache(有一級cache是共享的,如文中所示),而cache只是內(nèi)存的副本。那么這就帶來一個問題:如何保證每個cpu core中的cache是一致的?

在廣泛使用的cache一致性協(xié)議即MESI協(xié)議中,cacheline有四種狀態(tài):Modified、Exclusive、Shared、Invalid,分別表示修改、獨占、共享、無效。

當(dāng)某個cpu core寫一個內(nèi)存變量時,往往是(先)只修改cache,那么這就會導(dǎo)致不一致。為了保證一致,需要先把其他core的對應(yīng)的cacheline都invalid掉,給其他core們發(fā)送invalid消息,然后等待它們的response。

這個過程是耗時的,需要執(zhí)行寫變量的core等待,阻塞了它后面的操作。為了解決這個問題,cpu core往往有自己專屬的store buffer。

等待其他core給它response的時候,就可以先寫store buffer,然后繼續(xù)后面的讀操作,對外表現(xiàn)就是寫讀亂序。

因為寫操作是寫到store buffer中的,而store buffer是私有的,對其他core是透明的,core1無法訪問core2的store buffer。因此其他core讀不到這樣的修改。

這就是大概的原理。MESI協(xié)議非常復(fù)雜,背后的技術(shù)也很有意思。

總結(jié)

以上是生活随笔為你收集整理的深入探索并发编程之内存屏障:资源控制操作的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。