• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            posts - 200, comments - 8, trackbacks - 0, articles - 0

            C/C++ Volatile關(guān)鍵詞深度剖析

            Posted on 2015-01-13 20:35 鑫龍 閱讀(432) 評論(0)  編輯 收藏 引用 所屬分類: c++

            1    背景    1

            2    Volatile:易變的    1

            2.1    小結(jié)    2

            3    Volatile:不可優(yōu)化的    3

            3.1    小結(jié)    4

            4    Volatile:順序性    4

            4.1    happens-before    6

            4.2    小結(jié)    7

            5    Volatile:Java增強(qiáng)    8

            6    Volatile的起源    9

            7    參考資料    9

             

            1. 背景

            前幾天,發(fā)了一條如下的微博 (關(guān)于C/C++ Volatile關(guān)鍵詞的使用建議):


             

            此微博,引發(fā)了朋友們的大量討論:贊同者有之;批評者有之;當(dāng)然,更多的朋友,是希望我能更詳細(xì)的解讀C/C++ Volatile關(guān)鍵詞,來佐證我的微博觀點(diǎn)。而這,正是我寫這篇博文的初衷:本文,將詳細(xì)分析C/C++ Volatile關(guān)鍵詞的功能 (有多種功能)、Volatile關(guān)鍵詞在多線程編程中存在的問題、Volatile關(guān)鍵詞與編譯器/CPU的關(guān)系、C/C++ Volatile與Java Volatile的區(qū)別,以及Volatile關(guān)鍵詞的起源,希望對大家更好的理解、使用C/C++ Volatile,有所幫助。

             

            Volatile,詞典上的解釋為:易失的;易變的;易揮發(fā)的。那么用這個關(guān)鍵詞修飾的C/C++變量,應(yīng)該也能夠體現(xiàn)出”易變”的特征。大部分人認(rèn)識Volatile,也是從這個特征出發(fā),而這也是本文揭秘的C/C++ Volatile的第一個特征。

             

             

            1. Volatile:易變的

            在介紹C/C++ Volatile關(guān)鍵詞的”易變”性前,先讓我們看看以下的兩個代碼片段,以及他們對應(yīng)的匯編指令 (以下用例的匯編代碼,均為VS 2008編譯出來的Release版本):

             

            • 測試用例一:非Volatile變量

              volatile1

              b = a + 1;這條語句,對應(yīng)的匯編指令是:lea ecx, [eax + 1]。由于變量a,在前一條語句a = fn(c)執(zhí)行時,被緩存在了寄存器eax中,因此b = a + 1;語句,可以直接使用仍舊在寄存器eax中的a,來進(jìn)行計算,對應(yīng)的也就是匯編:[eax + 1]。

            • 測試用例二:Volatile變量

            volatile2

            與測試用例一唯一的不同之處,是變量a被設(shè)置為volatile屬性,一個小小的變化,帶來的是匯編代碼上很大的變化。a = fn(c)執(zhí)行后,寄存器ecx中的a,被寫回內(nèi)存:mov dword ptr [esp+0Ch], ecx。然后,在執(zhí)行b = a + 1;語句時,變量a有重新被從內(nèi)存中讀取出來:mov eax, dword ptr [esp + 0Ch],而不再直接使用寄存器ecx中的內(nèi)容。

            1. 小結(jié)

            從以上的兩個用例,就可以看出C/C++ Volatile關(guān)鍵詞的第一個特性:易變性。所謂的易變性,在匯編層面反映出來,就是兩條語句,下一條語句不會直接使用上一條語句對應(yīng)的volatile變量的寄存器內(nèi)容,而是重新從內(nèi)存中讀取。volatile的這個特性,相信也是大部分朋友所了解的特性。

             

            在了解了C/C++ Volatile關(guān)鍵詞的”易變”特性之后,再讓我們接著繼續(xù)來剖析Volatile的下一個特性:”不可優(yōu)化”特性。

             

            1. Volatile:不可優(yōu)化的

            與前面介紹的”易變”性類似,關(guān)于C/C++ Volatile關(guān)鍵詞的第二個特性:”不可優(yōu)化”性,也通過兩個對比的代碼片段來說明:

             

            • 測試用例三:非Volatile變量

              volatile4

              在這個用例中,非volatile變量a,b,c全部被編譯器優(yōu)化掉了 (optimize out),因為編譯器通過分析,發(fā)覺a,b,c三個變量是無用的,可以進(jìn)行常量替換。最后的匯編代碼相當(dāng)簡介,高效率。

            • 測試用例四:Volatile變量

              volatile3

              測試用例四,與測試用例三類似,不同之處在于,a,b,c三個變量,都是volatile變量。這個區(qū)別,反映到匯編語言中,就是三個變量仍舊存在,需要將三個變量從內(nèi)存讀入到寄存器之中,然后再調(diào)用printf()函數(shù)。

             

            1. 小結(jié)

            從測試用例三、四,可以總結(jié)出C/C++ Volatile關(guān)鍵詞的第二個特性:“不可優(yōu)化”特性。volatile告訴編譯器,不要對我這個變量進(jìn)行各種激進(jìn)的優(yōu)化,甚至將變量直接消除,保證程序員寫在代碼中的指令,一定會被執(zhí)行。相對于前面提到的第一個特性:”易變”性,”不可優(yōu)化”特性可能知曉的人會相對少一些。但是,相對于下面提到的C/C++ Volatile的第三個特性,無論是”易變”性,還是”不可優(yōu)化”性,都是Volatile關(guān)鍵詞非常流行的概念。

             

            1. Volatile:順序性

             

            C/C++ Volatile關(guān)鍵詞前面提到的兩個特性,讓Volatile經(jīng)常被解讀為一個為多線程而生的關(guān)鍵詞:一個全局變量,會被多線程同時訪問/修改,那么線程內(nèi)部,就不能假設(shè)此變量的不變性,并且基于此假設(shè),來做一些程序設(shè)計。當(dāng)然,這樣的假設(shè),本身并沒有什么問題,多線程編程,并發(fā)訪問/修改的全局變量,通常都會建議加上Volatile關(guān)鍵詞修飾,來防止C/C++編譯器進(jìn)行不必要的優(yōu)化。但是,很多時候,C/C++ Volatile關(guān)鍵詞,在多線程環(huán)境下,會被賦予更多的功能,從而導(dǎo)致問題的出現(xiàn)。

             

            回到本文背景部分我的那篇微博,我的這位朋友,正好犯了一個這樣的問題。其對C/C++ Volatile關(guān)鍵詞的使用,可以抽象為下面的偽代碼:

            v5

            這段偽代碼,聲明另一個Volatile的flag變量。一個線程(Thread1)在完成一些操作后,會修改這個變量。而另外一個線程(Thread2),則不斷讀取這個flag變量,由于flag變量被聲明了volatile屬性,因此編譯器在編譯時,并不會每次都從寄存器中讀取此變量,同時也不會通過各種激進(jìn)的優(yōu)化,直接將if (flag == true)改寫為if (false == true)。只要flag變量在Thread1中被修改,Thread2中就會讀取到這個變化,進(jìn)入if條件判斷,然后進(jìn)入if內(nèi)部進(jìn)行處理。在if條件的內(nèi)部,由于flag == true,那么假設(shè)Thread1中的something操作一定已經(jīng)完成了,在基于這個假設(shè)的基礎(chǔ)上,繼續(xù)進(jìn)行下面的other things操作。

             

            通過將flag變量聲明為volatile屬性,很好的利用了本文前面提到的C/C++ Volatile的兩個特性:”易變”性;”不可優(yōu)化”性。按理說,這是一個對于volatile關(guān)鍵詞的很好應(yīng)用,而且看到這里的朋友,也可以去檢查檢查自己的代碼,我相信肯定會有這樣的使用存在。

             

            但是,這個多線程下看似對于C/C++ Volatile關(guān)鍵詞完美的應(yīng)用,實(shí)際上卻是有大問題的。問題的關(guān)鍵,就在于前面標(biāo)紅的文字:由于flag = true,那么假設(shè)Thread1中的something操作一定已經(jīng)完成了。flag == true,為什么能夠推斷出Thread1中的something一定完成了?其實(shí)既然我把這作為一個錯誤的用例,答案是一目了然的:這個推斷不能成立,你不能假設(shè)看到flag == true后,flag = true;這條語句前面的something一定已經(jīng)執(zhí)行完成了。這就引出了C/C++ Volatile關(guān)鍵詞的第三個特性:順序性。

             

            同樣,為了說明C/C++ Volatile關(guān)鍵詞的”順序性”特征,下面給出三個簡單的用例 (注:與上面的測試用例不同,下面的三個用例,基于的是Linux系統(tǒng),使用的是”GCC: (Debian 4.3.2-1.1) 4.3.2″):

             

            • 測試用例五:非Volatile變量

              v9

              一個簡單的示例,全局變量A,B均為非volatile變量。通過gcc O2優(yōu)化進(jìn)行編譯,你可以驚奇的發(fā)現(xiàn),A,B兩個變量的賦值順序被調(diào)換了!!!在對應(yīng)的匯編代碼中,B = 0語句先被執(zhí)行,然后才是A = B + 1語句被執(zhí)行。

              在這里,我先簡單的介紹一下C/C++編譯器最基本優(yōu)化原理:保證一段程序的輸出,在優(yōu)化前后無變化。將此原理應(yīng)用到上面,可以發(fā)現(xiàn),雖然gcc優(yōu)化了A,B變量的賦值順序,但是foo()函數(shù)的執(zhí)行結(jié)果,優(yōu)化前后沒有發(fā)生任何變化,仍舊是A = 1;B = 0。因此這么做是可行的。

            • 測試用例六:一個Volatile變量

              v10

              此測試,相對于測試用例五,最大的區(qū)別在于,變量B被聲明為volatile變量。通過查看對應(yīng)的匯編代碼,B仍舊被提前到A之前賦值,Volatile變量B,并未阻止編譯器優(yōu)化的發(fā)生,編譯后仍舊發(fā)生了亂序現(xiàn)象。

              如此看來,C/C++ Volatile變量,與非Volatile變量之間的操作,是可能被編譯器交換順序的

              通過此用例,已經(jīng)能夠很好的說明,本章節(jié)前面,通過flag == true,來假設(shè)something一定完成是不成立的。在多線程下,如此使用volatile,會產(chǎn)生很嚴(yán)重的問題。但是,這不是終點(diǎn),請繼續(xù)看下面的測試用例七。

            • 測試用例七:兩個Volatile變量

              v11

              同時將A,B兩個變量都聲明為volatile變量,再來看看對應(yīng)的匯編。奇跡發(fā)生了,A,B賦值亂序的現(xiàn)象消失。此時的匯編代碼,與用戶代碼順序高度一直,先賦值變量A,然后賦值變量B。

              如此看來,C/C++ Volatile變量間的操作,是不會被編譯器交換順序的


            1. happens-before

             

            通過測試用例六,可以總結(jié)出:C/C++ Volatile變量與非Volatile變量間的操作順序,有可能被編譯器交換。因此,上面多線程操作的偽代碼,在實(shí)際運(yùn)行的過程中,就有可能變成下面的順序:

            v6

            由于Thread1中的代碼執(zhí)行順序發(fā)生變化,flag = true被提前到something之前進(jìn)行,那么整個Thread2的假設(shè)全部失效。由于something未執(zhí)行,但是Thread2進(jìn)入了if代碼段,整個多線程代碼邏輯出現(xiàn)問題,導(dǎo)致多線程完全錯誤。

             

            細(xì)心的讀者看到這里,可能要提問,根據(jù)測試用例七,C/C++ Volatile變量間,編譯器是能夠保證不交換順序的,那么能不能將something中所有的變量全部設(shè)置為volatile呢?這樣就阻止了編譯器的亂序優(yōu)化,從而也就保證了這個多線程程序的正確性。

             

            針對此問題,很不幸,仍舊不行。將所有的變量都設(shè)置為volatile,首先能夠阻止編譯器的亂序優(yōu)化,這一點(diǎn)是可以肯定的。但是,別忘了,編譯器編譯出來的代碼,最終是要通過CPU來執(zhí)行的。目前,市場上有各種不同體系架構(gòu)的CPU產(chǎn)品,CPU本身為了提高代碼運(yùn)行的效率,也會對代碼的執(zhí)行順序進(jìn)行調(diào)整,這就是所謂的CPU Memory Model (CPU內(nèi)存模型)。關(guān)于CPU的內(nèi)存模型,可以參考這些資料:Memory Ordering From WikiMemory Barriers Are Like Source Control Operations From Jeff PreshingCPU Cache and Memory Ordering From 何登成。下面,是截取自Wiki上的一幅圖,列舉了不同CPU架構(gòu),可能存在的指令亂序。

             

            mo

             

            從圖中可以看到,X86體系(X86,AMD64),也就是我們目前使用最廣的CPU,也會存在指令亂序執(zhí)行的行為:StoreLoad亂序,讀操作可以提前到寫操作之前進(jìn)行。

             

            因此,回到上面的例子,哪怕將所有的變量全部都聲明為volatile,哪怕杜絕了編譯器的亂序優(yōu)化,但是針對生成的匯編代碼,CPU有可能仍舊會亂序執(zhí)行指令,導(dǎo)致程序依賴的邏輯出錯,volatile對此無能為力。

             

            其實(shí),針對這個多線程的應(yīng)用,真正正確的做法,是構(gòu)建一個happens-before語義。關(guān)于happens-before語義的定義,可參考文章:The Happens-Before Relation。下面,用圖的形式,來展示happens-before語義:

             

            v7

             

            如圖所示,所謂的happens-before語義,就是保證Thread1代碼塊中的所有代碼,一定在Thread2代碼塊的第一條代碼之前完成。當(dāng)然,構(gòu)建這樣的語義有很多方法,我們常用的Mutex、Spinlock、RWLock,都能保證這個語義 (關(guān)于happens-before語義的構(gòu)建,以及為什么鎖能保證happens-before語義,以后專門寫一篇文章進(jìn)行討論)。但是,C/C++ Volatile關(guān)鍵詞不能保證這個語義,也就意味著C/C++ Volatile關(guān)鍵詞,在多線程環(huán)境下,如果使用的不夠細(xì)心,就會產(chǎn)生如同我這里提到的錯誤。

             

            1. 小結(jié)

             

            C/C++ Volatile關(guān)鍵詞的第三個特性:”順序性”,能夠保證Volatile變量間的順序性,編譯器不會進(jìn)行亂序優(yōu)化。Volatile變量與非Volatile變量的順序,編譯器不保證順序,可能會進(jìn)行亂序優(yōu)化。同時,C/C++ Volatile關(guān)鍵詞,并不能用于構(gòu)建happens-before語義,因此在進(jìn)行多線程程序設(shè)計時,要小心使用volatile,不要掉入volatile變量的使用陷阱之中。

             

            1. Volatile:Java增強(qiáng)

             

            在介紹了C/C++ Volatile關(guān)鍵詞之后,再簡單介紹一下Java的Volatile。與C/C++的Volatile關(guān)鍵詞類似,Java的Volatile也有這三個特性,但最大的不同在于:第三個特性,”順序性”,Java的Volatile有很極大的增強(qiáng),Java Volatile變量的操作,附帶了Acquire與Release語義。所謂的Acquire與Release語義,可參考文章:Acquire and Release Semantics。(這一點(diǎn),后續(xù)有必要的話,可以寫一篇文章專門討論)。Java Volatile所支持的Acquire、Release語義,如下:

             

            • 對于Java Volatile變量的寫操作,帶有Release語義,所有Volatile變量寫操作之前的針對其他任何變量的讀寫操作,都不會被編譯器、CPU優(yōu)化后,亂序到Volatile變量的寫操作之后執(zhí)行。

            • 對于Java Volatile變量的讀操作,帶有Acquire語義,所有Volatile變量讀操作之后的針對其他任何變量的讀寫操作,都不會被編譯器、CPU優(yōu)化后,亂序到Volatile變量的讀操作之前進(jìn)行。

             

            通過Java Volatile的Acquire、Release語義,對比C/C++ Volatile,可以看出,Java Volatile對于編譯器、CPU的亂序優(yōu)化,限制的更加嚴(yán)格了。Java Volatile變量與非Volatile變量的一些亂序操作,也同樣被禁止。

             

            由于Java Volatile支持Acquire、Release語義,因此Java Volatile,能夠用來構(gòu)建happens-before語義。也就是說,前面提到的C/C++ Volatile在多線程下錯誤的使用場景,在Java語言下,恰好就是正確的。如下圖所示:

             

            v8_new

             

            1. Volatile的起源

             

            C/C++的Volatile關(guān)鍵詞,有三個特性:易變性;不可優(yōu)化性;順序性。那么,為什么Volatile被設(shè)計成這樣呢?要回答這個問題,就需要從Volatile關(guān)鍵詞的產(chǎn)生說起。(注:這一小節(jié)的內(nèi)容,參考自C++ and the Perils of Double-Checked Locking論文的第10章節(jié):volatile:A Brief History。這是一篇頂頂好的論文,值得多次閱讀,強(qiáng)烈推薦!)

             

            Volatile關(guān)鍵詞,最早出現(xiàn)于19世紀(jì)70年代,被用于處理memory-mapeed I/O (MMIO)帶來的問題。在引入MMIO之后,一塊內(nèi)存地址,既有可能是真正的內(nèi)存,也有可能被映射到一個I/O端口。相對的,讀寫一個內(nèi)存地址,既有可能操作內(nèi)存,也有可能讀寫的是一個I/O設(shè)備。MMIO為什么需要引入Volatile關(guān)鍵詞?考慮如下的一個代碼片段:

            在此代碼片段中,指針p既有可能指向一個內(nèi)存地址,也有可能指向一個I/O設(shè)備。如果指針p指向的是I/O設(shè)備,那么(1),(2)中的a,b,就會接收到I/O設(shè)備的連續(xù)兩個字節(jié)。但是,p也有可能指向內(nèi)存,此時,編譯器的優(yōu)化策略,就可能會判斷出a,b同時從同一內(nèi)存地址讀取數(shù)據(jù),在做完(1)之后,直接將a賦值給b。對于I/O設(shè)備,需要防止編譯器做這個優(yōu)化,不能假設(shè)指針b指向的內(nèi)容不變——易變性。

             

            同樣,代碼(3),(4)也有類似的問題,編譯器發(fā)現(xiàn)將a,b同時賦值給指針p是無意義的,因此可能會優(yōu)化代碼(3)中的賦值操作,僅僅保留代碼(4)。對于I/O設(shè)備,需要防止編譯器將寫操作給徹底優(yōu)化消失了——”不可優(yōu)化”性。

             

            對于I/O設(shè)備,編譯器不能隨意交互指令的順序,因為順序一變,寫入I/O設(shè)備的內(nèi)容也就發(fā)生變化了——”順序性”。

             

            基于MMIO的這三個需求,設(shè)計出來的C/C++ Volatile關(guān)鍵詞,所含有的特性,也就是本文前面分析的三個特性:易變性;不可優(yōu)化性;順序性。

             

            1. 參考資料

            [1] Wiki. Volatile variable.

            [2] Wiki. Memory ordering.

            [3] Scott Meyers; Andrei Alexandrescu. C++ and the Perils of Double-Checked Locking.

            [4] Jeff Preshing. Memory Barriers Are Like Source Control Operations.

            [5] Jeff Preshing. The Happens-Before Relation.

            [6] Jeff Preshing. Acquire and Release Semantics.

            [7] 何登成. CPU Cache and Memory Ordering——并發(fā)程序設(shè)計入門.

            [8] Bartosz Milewski. Who ordered sequential consistency?

            [9] Andrew Haley. What are we going to do about volatile?

            [10] Java Glossary. volatile.

            [11] stackoverflow. Why is volatile not considered useful in multithreaded C or C++ programming?

            [12] msdn. Volatile fields.

            [13] msdn. volatile (C++).

            [14] 劉未鵬. 《C++0x漫談》系列之:多線程內(nèi)存模型.

            国内精品久久久久久久coent| 国产精品久久久久久影院| 精品乱码久久久久久夜夜嗨| 狠狠人妻久久久久久综合| 久久综合色之久久综合| 狠狠色狠狠色综合久久| 日韩精品无码久久久久久| 久久最近最新中文字幕大全 | 精品久久久久久久国产潘金莲| 亚洲国产综合久久天堂| 久久婷婷五月综合97色| 性做久久久久久免费观看| 久久久久AV综合网成人| 久久精品国产一区二区三区不卡| 99久久国产精品免费一区二区| 久久综合中文字幕| 久久国产色AV免费看| 久久笫一福利免费导航| www亚洲欲色成人久久精品| 国产aⅴ激情无码久久| 久久久久97国产精华液好用吗| 色偷偷88888欧美精品久久久| 色综合久久88色综合天天 | 精品久久久久久久| 色偷偷偷久久伊人大杳蕉| 久久久久亚洲精品日久生情| 精品久久久久久无码中文字幕| 久久99国内精品自在现线| 色偷偷偷久久伊人大杳蕉| 亚洲精品无码久久久影院相关影片| 久久性精品| 色综合久久久久综合99| 久久久网中文字幕| 亚洲欧美日韩精品久久| 国产一级持黄大片99久久| 国产精品久久久久久吹潮| 7777精品久久久大香线蕉| 久久精品人人做人人爽电影| 久久久久久国产a免费观看不卡| 国产精品久久久久久久午夜片| 青青青青久久精品国产|