同时存多个变量缓存 微信小程序_CPU缓存一致性协议MESI,memory barrier和java volatile...
MESI協(xié)議
MESI協(xié)議是一個(gè)被廣泛使用的CPU緩存一致性協(xié)議。我們都知道在CPU中存在著多級(jí)緩存,緩存級(jí)別越低,容量就越小,速度也越快。有了緩存,CPU就不需要每次都向主存讀寫數(shù)據(jù),這提高了CPU的運(yùn)行速度。然而,在多核CPU中,低級(jí)別的緩存是單個(gè)CPU獨(dú)占的:
如上圖所示,每個(gè)CPU核心分別擁有獨(dú)立的一、二級(jí)緩存,共享了三級(jí)緩存。這就帶來了緩存一致性的問題:當(dāng)同一份數(shù)據(jù)同時(shí)存在于多個(gè)CPU的獨(dú)立緩存中時(shí),如何保證緩存數(shù)據(jù)的一致性?
MESI協(xié)議提供了一種方式,成功的解決了緩存一致性問題。對(duì)于緩存中的每一行,都設(shè)置一個(gè)狀態(tài)位,一共有四種狀態(tài):
- M(modified):表示緩存行僅存在于當(dāng)前的緩存中,并且已經(jīng)被更改。在該緩存行寫回到主存之前,任何其他CPU都不能讀取該緩存行的內(nèi)容。
- E(exclusive):表示緩存行僅存在于當(dāng)前的緩存中,并且未被修改。如果有其他CPU讀取該行,則轉(zhuǎn)移到Shared狀態(tài);如果修改該行,則轉(zhuǎn)移到Modified狀態(tài)。
- S(shared):表示有多個(gè)CPU共享該緩存行,且內(nèi)容未被修改。
- I(invalid):表示緩存行已失效(未被使用)。
從上述狀態(tài)定義可以看出,MESI協(xié)議實(shí)際上定義了一個(gè)狀態(tài)機(jī),其中狀態(tài)轉(zhuǎn)移規(guī)則保證了CPU在多級(jí)緩存環(huán)境下的緩存一致性。MESI定義的狀態(tài)轉(zhuǎn)移規(guī)則如下所述:
除Invalid狀態(tài)以外,所有狀態(tài)的緩存行都可以進(jìn)行讀操作
從狀態(tài)定義就可以看出,只有Invalid狀態(tài)的緩存行內(nèi)容是無效的,必須從主存讀取。
只有在狀態(tài)為M或E時(shí)才能進(jìn)行寫操作,如果當(dāng)前狀態(tài)為S,則其它CPU中的同一行必須轉(zhuǎn)移到狀態(tài)I,這是通過發(fā)送RFO(request for ownership)廣播實(shí)現(xiàn)的
M或E狀態(tài)下,緩存行都只存在于單個(gè)CPU中,S狀態(tài)下多個(gè)CPU共享一行,因此必須將其他CPU的狀態(tài)置為I,才能進(jìn)行寫操作。
除M以外的任意狀態(tài)都可轉(zhuǎn)移到I,M狀態(tài)必須先寫回到主存再丟棄
只要內(nèi)容未被修改,CPU可以再任意時(shí)刻丟棄一個(gè)緩存行,否則則必須先把修改的內(nèi)容寫回到主存
處于M狀態(tài)的緩存必須攔截其他CPU對(duì)同一行的讀操作,并返回自身緩存中的數(shù)據(jù)
這可以保證所有CPU讀到的都是最新的內(nèi)容,這種攔截操作稱為snoop,數(shù)據(jù)不需要寫回到主存,直接由M狀態(tài)的緩存返回,狀態(tài)由M轉(zhuǎn)移到S
處于S狀態(tài)的緩存必須監(jiān)聽RFO廣播,并轉(zhuǎn)移到I狀態(tài)
當(dāng)一個(gè)CPU修改S狀態(tài)的緩存時(shí),其余的緩存必須先轉(zhuǎn)移到I狀態(tài),防止并發(fā)的寫操作
處于E狀態(tài)的緩存必須攔截其他CPU的讀操作,并轉(zhuǎn)移到S狀態(tài)
當(dāng)有其他CPU讀E狀態(tài)的緩存時(shí),狀態(tài)必須由獨(dú)占轉(zhuǎn)移到共享
上述過程可以用下面這幅圖來描述:
那么問題來了
MESI很好的解決了緩存一致性的問題,但是也不可避免的帶來了額外的開銷。考慮一個(gè)簡單的賦值操作a=1,假設(shè)變量所在的緩存行不在當(dāng)前CPU中,則CPU需要發(fā)送"read invalidate"消息,獲取緩存行,并且告知其他CPU丟棄該緩存行,然后該CPU必須等待,直到收到來自其他CPU的確認(rèn)響應(yīng)為止:
而實(shí)際上,無論a之前的值為何,在該條指令執(zhí)行后都會(huì)被覆蓋,因此這段等待的開銷是完全沒有必要的。為此,CPU的設(shè)計(jì)者加入了store buffer,用于緩存store指令對(duì)緩存行的修改:
如圖所示,對(duì)緩存行的修改操作不會(huì)立刻執(zhí)行到緩存行上,而是先進(jìn)入store buffer,這樣CPU的寫操作就不需要等待到從其他CPU得到緩存行才能執(zhí)行。CPU可以立即執(zhí)行寫操作,等到得到緩存行時(shí),才將變更從store buffer寫入緩存行。然而,這又立刻帶來了另外一個(gè)問題,由于store buffer的存在,在CPU中同一個(gè)變量可能存在兩份拷貝(當(dāng)緩存行到達(dá)CPU時(shí),緩存和store buffer中存在同一個(gè)變量的兩份拷貝),這無疑破壞了緩存的一致性,若CPU在store buffer寫入緩存之前l(fā)oad數(shù)據(jù),就會(huì)拿到舊的數(shù)據(jù)。為了解決這個(gè)問題,CPU設(shè)計(jì)者又加入了store forwarding機(jī)制,簡單的講就是CPU會(huì)優(yōu)先從store buffer中取變量,保證同一時(shí)刻一個(gè)變量在單個(gè)CPU中的一致性:
然而,這樣做并不能解決另外一個(gè)問題,那就是隱式的數(shù)據(jù)依賴,考慮下面兩個(gè)
代碼清單1:foo(){ a = 1; b = 1;}bar(){ while(b == 0) continue; assert(a == 1);}復(fù)制代碼假設(shè)CPU0執(zhí)行foo,CPU1執(zhí)行bar,并且a處于CPU1的緩存中。由于store buffer的存在,對(duì)a的寫操作會(huì)立刻執(zhí)行,而不會(huì)等待其他CPU的invalidate響應(yīng)。CPU0接著執(zhí)行b=1,CPU1獲取到最新的b以后,執(zhí)行assert語句,此時(shí),CPU1有可能尚未收到來自CPU0的invalidate消息,因而a有可能仍在CPU1的緩存中,并且值未被改變,從而導(dǎo)致assert失敗。
內(nèi)存屏障
引入store buffer帶來了性能的提升,卻導(dǎo)致MESI協(xié)議無法保障緩存的一致性。從上一節(jié)中的例子可以看出,一致性問題的出現(xiàn)來源于數(shù)據(jù)之間的隱式依賴,也就是說必須保證某個(gè)操作在另外一個(gè)操作之前完成。比如a=1這個(gè)操作必須寫入到cache line(只有在cpu收到invalidate響應(yīng)時(shí),才會(huì)把數(shù)據(jù)從store buffer寫入cache line),才能執(zhí)行b=1。但是CPU是無法探測到這種隱式相關(guān)性的,必須由程序員自己來進(jìn)行控制。因此CPU提供了內(nèi)存屏障指令,該指令使得屏障之前的寫操作都在屏障之后的寫操作之前完成:
代碼清單2:foo(){ a = 1; smp_mb(); // 加入內(nèi)存屏障 b = 1;}bar(){ while(b == 0) continue; assert(a == 1);}復(fù)制代碼smp_mb的實(shí)際功能是對(duì)store buffer中的變量標(biāo)記,這樣當(dāng)CPU0執(zhí)行b=1時(shí),發(fā)現(xiàn)store buffer中存在標(biāo)記過的變量,就不能立刻將b=1寫入緩存行,而是將其寫入store buffer(但不進(jìn)行標(biāo)記)。等到CPU0收到invalidate響應(yīng),將store buffer中的標(biāo)記變量寫入緩存行,b=1才會(huì)寫入到緩存行。在此期間,由于標(biāo)記變量的存在,所有對(duì)b的讀操作都只能讀到b的原始值,也就是0,導(dǎo)致CPU1無法執(zhí)行到assert語句。
除了寫操作等待,invalidate操作的開銷也很大,因?yàn)樗拇嬖?#xff0c;CPU不得不頻繁丟棄緩存行,導(dǎo)致緩存命中率低下。為了進(jìn)一步提升性能,CPU中又加入了invalidate隊(duì)列(invalidate queue),CPU收到invalidate消息以后會(huì)立刻發(fā)送響應(yīng),但并不立刻處理,而是將該消息放入隊(duì)列,等到適當(dāng)?shù)臅r(shí)候再處理。與store buffer類似,這么做的副作用也是破壞了MESI協(xié)議,延遲響應(yīng)的代價(jià)就是緩存中可能存在過期的數(shù)據(jù)。這個(gè)問題同樣可以用內(nèi)存屏障來解決:
代碼清單3:foo(){ a = 1; smp_mb(); // 加入內(nèi)存屏障 b = 1;}bar(){ while(b == 0) continue; smp_mb(); // 加入內(nèi)存屏障 assert(a == 1);}復(fù)制代碼bar()函數(shù)的內(nèi)存屏障保證了屏障之前的invalidate消息都會(huì)執(zhí)行,然后才執(zhí)行后面的指令。這樣CPU1執(zhí)行assert時(shí),發(fā)現(xiàn)a的緩存行已經(jīng)失效,只能嘗試讀取,此時(shí)CPU0會(huì)返回最新的數(shù)據(jù)a=1,assert執(zhí)行成功。為了進(jìn)一步提升效率,CPU還支持對(duì)store buffer和invalidate隊(duì)列單獨(dú)進(jìn)行操作,這就是寫屏障和讀屏障。寫屏障保證屏障之前的寫操作對(duì)其他CPU都是可見的;讀屏障保證屏障之后的讀操作讀到的都是最新的數(shù)據(jù)。
Java中的內(nèi)存屏障
Java中的 volatile 關(guān)鍵字可以用來修飾變量,它可以保證:
可見性 一個(gè)線程對(duì)volatile變量的寫操作可以立刻被其他線程看到
原子性 對(duì)volatile變量的單個(gè)讀/寫操作具有原子性
在某些jvm中,對(duì) long 和 double 的讀寫操作是不具有原子性的,而是會(huì)拆成兩部分:對(duì)高32位和低32位分別賦值。因此,假設(shè)線程a在讀 long 變量l時(shí),線程b也在寫入,那么線程a可能讀到的數(shù)據(jù)可能一半是新的,一半是舊的。如果將 long 和 double 變量聲明為volatile,則可能保證變量的讀寫具有原子性。但是要注意,這個(gè)原子性只是對(duì)讀寫的單個(gè)操作而言的,對(duì)于復(fù)合操作則不能保證:
代碼清單4:volatile int a = 0;incrementAndGet(){ a++; return a;}復(fù)制代碼如果指望代碼清單4中的a變量能夠正確的增長,恐怕要失望了。因?yàn)閍++這個(gè)操作實(shí)際上是由讀取-修改-寫入三個(gè)操作組成的,在并發(fā)環(huán)境中,這樣的操作不具有原子性,數(shù)據(jù)更新很有可能會(huì)丟失。
可見性 又是怎么一回事呢?這就涉及到了內(nèi)存屏障,為了使得對(duì)volatile變量的修改對(duì)其他線程總是可見的,jvm會(huì)執(zhí)行如下操作:
在volatile變量的寫操作之后插入寫屏障
插入寫屏障之后,屏障之前的寫操作對(duì)于其他CPU都是可見的,需要注意的是此處的可見性并不只針對(duì)標(biāo)記為volatile的變量,而在所有在屏障之前執(zhí)行了寫操作的共享變量(寫屏障是對(duì)store buffer中存在的所有變量進(jìn)行標(biāo)記)。
在對(duì)volatile變量的讀操作之前插入讀屏障
插入讀屏障之后,本地緩存中所有被更改過的共享變量會(huì)立刻失效(通過執(zhí)行invalidate隊(duì)列中的消息實(shí)現(xiàn))。這樣,在屏障之后讀取共享變量時(shí),由于緩存失效,只能向主存或其他CPU發(fā)送讀取請(qǐng)求,從而保證了讀到的一定是最新的值。
代碼清單5是一個(gè)簡單的實(shí)例
代碼清單5:class MBExample{ int a = 0; volatile boolean flag = false; void foo(){ a = 2; flag = true; } void bar(){ if(flag) a ++; else a --; }}復(fù)制代碼假設(shè)線程t1和t2共享MBExample的一個(gè)實(shí)例,t1執(zhí)行foo, t2執(zhí)行bar。假設(shè)foo先于bar執(zhí)行,此時(shí)我們一定期望a最終的值為3。但是,如果flag變量未被標(biāo)記為 volatile ,根據(jù)之前的討論,由于store buffer和invalidate queue的存在,t2未必能獲得最新的a和flag的值(例如假設(shè)一開始a以S狀態(tài)存在于t1,t2的cache line中,而flag以E狀態(tài)存在與t1的cache line,最終的結(jié)果有可能為a=1)。如果flag被標(biāo)記為volatile,代碼清單5實(shí)際上變成了如代碼清單6的情形:
代碼清單6:...void foo(){ a = 2; // ---------------------------1 flag = false; // --------------------2 smp_wmb(); // 寫屏障}void bar(){ smp_rmb(); // 讀屏障 if(flag) // -------------------------3 a ++; // -----------------------4 else a --;}...復(fù)制代碼事實(shí)上,volatile的作用不止于此,對(duì)于JIT編譯器而言,volatile還是"指令屏障",如果編譯器出于性能優(yōu)化的考慮對(duì)指令進(jìn)行重排序,有可能破壞程序的原本意圖,volatile對(duì)這一行為進(jìn)行了限制。Java內(nèi)存模型針對(duì)volatile的指令重排序做了如下規(guī)定:
- 當(dāng)?shù)诙€(gè)操作是volatile寫時(shí),不管第一個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后。
- 當(dāng)?shù)谝粋€(gè)操作是volatile讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前。
- 當(dāng)?shù)谝粋€(gè)操作是volatile寫,第二個(gè)操作是volatile讀時(shí),不能重排序。
因此,當(dāng)foo在bar之前執(zhí)行時(shí),實(shí)際上產(chǎn)生了一種偏序關(guān)系,如代碼清單6所示1 >> 2 >> 3 >> 4,最終使得4中讀取到的a的值一定為2。這種指令重排的約束僅對(duì)JIT生效,因?yàn)閖ava字節(jié)碼解釋器的解釋執(zhí)行是line by line的,指令的先后順序天然的得到保留。
參考資料
Memory Barriers: a Hardware View for Software Hackers
MESI protocol
深入理解Java內(nèi)存模型(四)——volatile
Memory Barriers/Fences
Non-atomic Treatment of double and long
我自己收集了一些Java資料,里面就包涵了一些BAT面試資料,以及一些 Java 高并發(fā)、分布式、微服務(wù)、高性能、源碼分析、JVM等技術(shù)資料
資料獲取方式:關(guān)注我并私信“666”即可免費(fèi)獲取
總結(jié)
以上是生活随笔為你收集整理的同时存多个变量缓存 微信小程序_CPU缓存一致性协议MESI,memory barrier和java volatile...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 小例子 源码 莫凡_100
- 下一篇: rpa 手机_容智正式推出全球首款移动端