青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

colorful

zc qq:1337220912

 

剖析為什么在多核多線程程序中要慎用volatile關鍵字?

這篇文章詳細剖析了為什么在多核時代進行多線程編程時需要慎用volatile關鍵字。

主要內容有:
1. C/C++中的volatile關鍵字
2. Visual Studio對C/C++中volatile關鍵字的擴展
3. Java/.NET中的volatile關鍵字
4. Memory Model(內存模型)
5. Volatile使用建議

1. C/C++中的volatile關鍵字

1.1 傳統用途

C/C++作為系統級語言,它們與硬件的聯系是很緊密的。volatile的意思是“易變的”,這個關鍵字最早就是為了針對那些“異常”的內存操作 而準備的。它的效果是讓編譯器不要對這個變量的讀寫操作做任何優化,每次讀的時候都直接去該變量的內存地址中去讀,每次寫的時候都直接寫到該變量的內存地 址中去,即不做任何緩存優化。它經常用在需要處理中斷的嵌入式系統中,其典型的應用有下面幾種:

a. 避免用通用寄存器對內存讀寫的優化。編譯器常做的一種優化就是:把常用變量的頻繁讀寫弄到通用寄存器中,最后不用的時候再存回內存中。但是如果某個內存地址中的值是由片外決定的(例如另一個線程或是另一個設備可能更改它),那就需要volatile關鍵字了。(感謝Kenny老師指正)
b. 硬件寄存器可能被其他設備改變的情況。例如一個嵌入式板子上的某個寄存器直接與一個測試儀器連在一起,這樣在這個寄存器的值隨時可能被那個測試儀器更改。 在這種情況下如果把該值設為volatile屬性的,那么編譯器就會每次都直接從內存中去取這個值的最新值,而不是自作聰明的把這個值保留在緩存中而導致 讀不到最新的那個被其他設備寫入的新值。
c. 同一個物理內存地址M有兩個不同的內存地址的情況。例如兩個程序同時對同一個物理地址進行讀寫,那么編譯器就不能假設這個地址只會有一個程序訪問而做緩存優化,所以程序員在這種情況下也需要把它定義為volatile的。

1.2 多線程程序中的錯誤用法

看到這里,很多朋友自然會想到:恩,那么如果是兩個線程需要同時訪問一個共享變量,為了讓其中兩個線程每次都能讀到這個變量的最新值,我們就把它定 義為volatile的就好了嘛!我想這個就是多線程程序中volatile之所以引起那么多爭議的最大原因。可惜的是,這個想法是錯誤的。

舉例來說,想用volatile變量來做同步(例如一個flag)?錯!為什么?很簡單,雖然volatile意味著每次讀和寫都是直接去內存地址中去操作,但是volatile在C/C++現有標準中即不能保證原子性(Atomicity)也不能保證順序性(Ordering),所以幾乎所有試圖用volatile來進行多線程同步的方案都是錯的。我之前一篇文章介紹了Sequential Consistency模型(后 面簡稱SC),它其實就是我們印象中多線程程序應該有的執行順序。但是,SC最大的問題是性能太低了,因為CPU/編譯器完全沒有必要嚴格按代碼規定的順 序(program order)來執行每一條指令。學過體系結構的同學應該知道不管是編譯器也好CPU也好,他們最擅長做的事情就是幫你做亂序優化。在串行時代這些亂序優化 對程序員來說都是透明的,封裝好了的,你不用關心它們到底給你亂序成啥樣了,因為它們會保證優化后的程序的運行結果跟你寫程序時預期的結果是一模一樣的。 但是進入多核時代之后,CPU和編譯器還會繼續做那些串行時代的優化,更重要的是這些優化還會打破你多線程程序的SC模型語義,從而使得多線程程序的實際 運行結果與我們所期待的運行結果不一致!

拿X86來說,它的多核內存模型沒有嚴格執行SC,即屬于weak ordering(或者叫relax ordering?)。它唯一允許的亂序優化是可以把對不同地址的load操作提到store之前去(即把store x->load y亂序優化成load y -> store x)。而store x -> store y、load x -> load y,以及store x -> load y不允許交換執行順序。在X86這樣的內存模型下,volatile關鍵字根本就不能保證對不同volatile變量x和y的store x -> load y的操作不會被CPU亂序優化成load y -> store x。

而對多線程讀寫操作的原子性來說,諸如volatile x=1這樣的寫操作的原子性其實是由X86硬件保證的,跟volatile沒有任何關系。事實上,volatile根本不能保證對沒有內存對齊的變量(或者超出機器字長的變量)的讀寫操作的原子性。

為了有個更直觀的理解,我們來看看CPU的亂序優化是如何讓volatile在多線程程序中顯得如此無力的。下面這個著名的Dekker算法是想用 flag1/2和turn來實現兩個線程情況下的臨界區互斥訪問。這個算法關鍵就在于對flag1/2和turn的讀操作(load)是在其寫操作 (store)之后的,因此這個多線程算法能保證dekker1和dekker2中對gSharedCounter++的操作是互斥的,即等于是把 gSharedCounter++放到臨界區里去了。但是,多核X86可能會對這個store->load操作做亂序優化,例如dekker1中對 flag2的讀操作可能會被提到對flag1和turn的寫操作之前,這樣就會最終導致臨界區的互斥訪問失效,而gSharedCounter++也會因 此產生data race從而出現錯誤的計算結果。那么為什么多核CPU會對多線程程序做這樣的亂序優化呢?因為從單線程的視角來看flag2和flag1、turn是沒 有依賴關系的,所以CPU當然可以對他們進行亂序優化以便充分利用好CPU里面的流水線(想了解更多細節請參考計算機體系結構相關書籍)。這樣的優化雖然 從單線程角度來講沒有錯,但是它卻違反了我們設計這個多線程算法時所期望的那個多線程語義。(想要解決這個bug就需要自己手動添加memory barrier,或者干脆別去實現這樣的算法,而是使用類似pthread_mutex_lock這樣的庫函數,后面我會再講到這點)

當然,對不同的CPU來說他們的內存模型是不同的。比如說,如果這個程序是在單核上以多線程的方式執行那么它肯定不會出錯,因為單核CPU的內存模 型是符合SC的。而在例如PowerPC,ARM之類的架構上運行結果到底如何就得去翻它們的硬件手冊中內存模型是怎么定義的了。

2. Visual Studio對C/C++中volatile關鍵字的擴展

雖然C/C++中的volatile關鍵字沒有對ordering做任何保證,但是微軟從Visual Studio 2005開始就對volatile關鍵字添加了同步語義(保證ordering),即:對volatile變量的讀操作具有acquire語義,對 volatile變量的寫操作具有release語義。Acquire和Release語義是來自data-race-free模型的概念。為了理解這個 acquire語義和release語義有什么作用,我們來看看MSDN中的一個例子

例子中的 while (Sentinel) Sleep(0); // volatile spin lock 是對volatile變量的讀操作,它具有acquire語義,acquire語義的隱義是當前線程在對sentinel的這個讀操作之后的所有的對全局 變量的訪問都必須在該操作之后執行;同理,例子中的Sentinel = false; // exit critical section 是對volatile變量的寫操作,它具有release語義,release語義的隱義是當前線程在對sentinel這個寫操作之前的所有對全局變量 的訪問都必須在該操作之前執行完畢。所以ThreadFunc1()讀CriticalData時必定已經在ThreadFunc2()執行完 CriticalData++之后,即CriticalData最后輸出的值必定為1。建議大家用紙畫一下acquire/release來加深理解。一個比較形象的解釋就是把acquire當成lock,把release當成unlock,它倆組成了一個臨界區,所有臨界區外面的操作都只能往這個里面移,但是臨界區里面的操作都不能往外移,簡單吧?

其實這個程序就相當于用volatile變量的acquire和release語義實現了一個臨界區,在臨界區內部的代碼就是 Sleep(2000); CriticalData++; 或者更貼切點也可以看成是一對pthread_cond_wait和pthread_cond_signal。

這個volatile的acquire和release語義是VS自己的擴展,C/C++標準里是沒有的,所以同樣的代碼用gcc編譯執行結果就可 能是錯的,因為編譯器/CPU可能做違反正確性的亂序優化。Acquire和release語義本質上就是為了保證程序執行時memory order的正確性。但是,雖然這個VS擴展使得volatile變量能保證ordering,它還是不能保證對volatile變量讀寫的原子性。事 實上,如果我們的程序是跑在X86上面的話,內存對齊了的變量的讀寫的原子性是由硬件保證的,跟volatile沒有任何關系。而像volatile g_nCnt++這樣的語句本身就不是原子操作,想要保證這個操作是原子的,就必須使用帶LOCK語義的++操作,具體請看我這篇文章

另外,VS生成的volatile變量的匯編代碼是否真的調用了memory barrier也得看具體的硬件平臺,例如x86上就不需要使用memory barrier也能保證acquire和release語義,因為X86硬件本身就有比較強的memory模型了,但是Itanium上面VS就會生成帶 memory barrier的匯編代碼。具體可以參考這篇

但是,雖然VS對volatile關鍵字加入了acquire/release語義,有一種情況還是會出錯,即我們之前看到的dekker算法的例子。這 個其實蠻好理解的,因為讀操作的acquire語義不允許在其之后的操作往前移,但是允許在其之前的操作往后移;同理,寫操作的release語義允許在 其之后的操作往前移,但是不允許在其之前的操作往后移;這樣的話對一個volatile變量的讀操作(acquire)當然可以放到對另一個 volatile變量的寫操作(release)之前了!Bug就是這樣產生的!下面這個程序大家拿Visual Studio跑一下就會發現bug了(我試了VS2008和VS2010,都有這個bug)。多線程編程復雜吧?希望大家還沒被弄暈,要是暈了的話也很正 常,仔仔細細重新再看一遍吧:)

想解決這個Bug也很簡單,直接在dekker1和dekker2中對flag1/flag2/turn賦值操作之后都分別加入full memory barrier就可以了,即保證load一定是在store之后執行即可。具體的我就不詳述了。

3. Java/.NET中的volatile關鍵字

3.1 多線程語義

Java和.NET分別有JVM和CLR這樣的虛擬機,保證多線程的語義就容易多了。說簡單點,Java和.NET中的volatile關鍵字也是限制虛擬機做優化,都具有acquire和release語義,而且由虛擬機直接保證了對volatile變量讀寫操作的原子性。 (volatile 只保證可見性,不保證原子性。java中,對volatile修飾的long和double的讀寫就不是原子的 (http://java.sun.com/docs/books/jvms/second_edition/html /Threads.doc.html#22244),除此之外的基本類型和引用類型都是原子的。– 多謝liuchangit指正) 這 里需要注意的一點是,Java和.NET里面的volatile沒有對應于我們最開始提到的C/C++中對“異常操作”用volatile修飾的傳統用 法。原因很簡單,Java和.NET的虛擬機對安全性的要求比C/C++高多了,它們才不允許不安全的“異常”訪問存在呢。

而且像JVM/.NET這樣的程序可移植性都非常好。雖然現在C++1x正在把多線程模型添加到標準中去,但是因為C++本身的性質導致它的硬件平 臺依賴性很高,可移植性不是特別好,所以在移植C/C++多線程程序時理解硬件平臺的內存模型是非常重要的一件事情,它直接決定你這個程序是否會正確執 行。

至于Java和.NET中是否也存在類似VS 2005那樣的bug我沒時間去測試,道理其實是相同的,真有需要的同學自己應該能測出來。好像這篇InfoQ的文章中顯示Java運行這個dekker算法沒有問題,因為JVM給它添加了mfence。另一個臭名昭著的例子就應該是Double-Checked Locking了。

3.2 volatile int與AtomicInteger區別

Java和.NET中這兩者還是有些區別的,主要就是后者提供了類似incrementAndGet()這樣的方法可以直接調用(保證了原子性), 而如果是volatile x進行++操作則不是原子的。increaseAndGet()的實現調用了類似CAS這樣的原子指令,所以能保證原子性,同時又不會像使用 synchronized關鍵字一樣損失很多性能,用來做全局計數器非常合適。

4. Memory Model(內存模型)

說了這么多,還是順帶介紹一下Memory Model吧。就像前面說的,CPU硬件有它自己的內存模型,不同的編程語言也有它自己的內存模型。如果用一句話來介紹什么是內存模型,我會說它就是程序 員,編程語言和硬件之間的一個契約,它保證了共享的內存地址里的值在需要的時候是可見的。下次我會專門詳細寫一篇關于它的內容。它最大的作用是取得可編程 性與性能優化之間的一個平衡。

5. volatile使用建議

總的來說,volatile關鍵字有兩種用途:一個是ISO C/C++中用來處理“異常”內存行為(此用途只保證不讓編譯器做任何優化,對多核CPU是否會進行亂序優化沒有任何約束力),另一種是在Java /.NET(包括Visual Studio添加的擴展)中用來實現高性能并行算法(此種用途通過使用memory barrier保證了CPU/編譯器的ordering,以及通過JVM或者CLR保證了對該volatile變量讀寫操作的原子性)。

一句話,volatile對多線程編程是非常危險的,使用的時候千萬要小心你的代碼在多核上到底是不是按你所想的方式執行的,特別是對現在暫時還沒 有引入內存模型的C/C++程序更是如此。安全起見,大家還是用Pthreads,Java.util.concurrent,TBB等并行庫提供的 lock/spinlock,conditional variable, barrier, Atomic Variable之類的同步方法來干活的好,因為它們的內部實現都調用了相應的memory barrier來保證memory ordering,你只要保證你的多線程程序沒有data race,那么它們就能幫你保證你的程序是正確的(是的,Pthreads庫也是有它自己的內存模型的,只不過它的內存模型還些缺點,所以把多線程內存模 型直接集成到C/C++中是更好的辦法,也是將來的趨勢,但是C++1x中將不會像Java/.NET一樣給volatile關鍵字添加acquire和 release語義,而是轉而提供另一種具有同步語義的atomic variables,此為后話)。如果你想實現更高性能的lock free算法,或是使用volatile來進行同步,那么你就需要先把CPU和編程語言的memory model搞清楚,然后再時刻注意Atomicity和Ordering是否被保證了。(注 意,用沒有acquire/release語義的volatile變量來進行同步是錯誤的,但是你仍然可以在C/C++中用volatile來修飾一個不 是用來做同步(例如一個event flag)而只是被不同線程讀寫的共享變量,只不過它的新值什么時候能被另一個線程讀到是沒有保證的,需要你自己做相應的處理)

Herb Sutter 在他的那篇volatile vs. volatile中對這兩種用法做了很仔細的區分,我把其中兩張表格鏈接貼過來供大家參考:

volatile的兩種用途
volatile兩種用途的異同

最后附上《Java Concurrency in Practice》3.1.4節中對Java語言的volatile關鍵字的使用建議(不要被英語嚇到,這些內容確實對你有用,而且還能順便幫練練英語,哈哈):

So from a memory visibility perspective, writing a volatile variable is like exiting a synchronized block and reading a volatile variable is like entering a synchronized block. However, we do not recommend relying too heavily on volatile variables for visibility; code that relies on volatile variables for visibility of arbitrary state is more fragile and harder to understand than code that uses locking.

Use volatile variables only when they simplify implementing and verifying your synchronization policy; avoid using volatile variables when veryfing correctness would require subtle reasoning about visibility. Good uses of volatile variables include ensuring the visibility of their own state, that of the object they refer to, or indicating that an important lifecycle event (such as initialization or shutdown) has occurred.

Locking can guarantee both visibility and atomicity; volatile variables can only guarantee visibility.

You can use volatile variables only when all the following criteria are met:
(1) Writes to the variable do not depend on its current value, or you can ensure that only a single thread ever updates the value;
(2) The variable does not participate in invariants with other state variables; and
(3) Locking is not required for any other reason while the variable is being accessed.

參考資料

1. 《Java Concurrency in Practice》3.1.4節
2. volatile vs. volatile(Herb Sutter對volatile的闡述,必看)
3. The “Double-Checked Locking is Broken” Declaration
4. Threading in C#
5. Volatile: Almost Useless for Multi-Threaded Programming
6. Memory Ordering in Modern Microprocessors
7. Memory Ordering @ Wikipedia
8. 內存屏障什么的
9. The memory model of x86
10. VC 下 volatile 變量能否建立 Memory Barrier 或并發鎖
11. Sayonara volatile(Concurrent Programming on Windows作者的文章 跟我觀點幾乎一致)
12. Java 理論與實踐: 正確使用 Volatile 變量
13. Java中的Volatile關鍵字

 

轉自:http://www.parallellabs.com/2010/12/04/why-should-we-be-care-of-volatile-keyword-in-multithreaded-applications/

posted on 2012-07-02 17:03 多彩人生 閱讀(466) 評論(0)  編輯 收藏 引用


只有注冊用戶登錄后才能發表評論。
網站導航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


導航

統計

常用鏈接

留言簿(3)

隨筆分類

隨筆檔案

搜索

最新評論

閱讀排行榜

評論排行榜

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <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>
            国产日韩欧美不卡| 亚洲国产va精品久久久不卡综合| 国语自产精品视频在线看抢先版结局 | 久久久久久久精| 欧美多人爱爱视频网站| 久久男人资源视频| 国产午夜亚洲精品不卡| 久久久免费观看视频| 国产精品久久一卡二卡| 一本久久青青| 亚洲免费一级电影| 国产精品视频一| 欧美电影免费观看高清| 国产一区二区高清视频| 欧美一区二区精美| 蜜臀av一级做a爰片久久| 在线播放亚洲| 亚洲影院在线观看| 久久国产成人| 亚洲精品一区二区三区婷婷月| 黑人一区二区| 亚洲视频综合| 久久国产精品久久国产精品| 国产日韩欧美黄色| 久久一区二区三区av| 最新亚洲激情| 亚洲国产另类久久久精品极度| 免费不卡在线观看| 91久久久久久久久| 亚洲欧美精品| 一区二区三区无毛| 玖玖视频精品| 在线综合亚洲欧美在线视频| 欧美一区免费视频| 亚洲欧洲一区二区三区在线观看| 欧美韩国一区| 亚洲久久在线| 久久美女性网| 亚洲午夜视频| 亚洲国产美女| 国产欧美一区二区三区在线老狼| 久久琪琪电影院| 一区二区久久久久久| 久久精品动漫| 亚洲精品亚洲人成人网| 国产精品婷婷午夜在线观看| 亚洲午夜视频在线| 欲色影视综合吧| 欧美视频一区二区三区| 久久久久免费视频| 亚洲无线视频| 亚洲第一成人在线| 国产精品嫩草99av在线| 免费成人你懂的| 制服诱惑一区二区| 欧美国产精品日韩| 久久久久久久久伊人| 亚洲国产精品99久久久久久久久| 欧美日韩成人网| 久久综合给合久久狠狠色 | 黑人巨大精品欧美一区二区| 老司机午夜精品视频| 亚洲一区在线免费| 亚洲美女免费精品视频在线观看| 久久久久久夜| 一区二区不卡在线视频 午夜欧美不卡在 | 久久久久久夜精品精品免费| 国产综合色在线| 欧美成人综合| 亚洲欧美清纯在线制服| 99热在线精品观看| 亚洲高清免费| 欧美第十八页| 奶水喷射视频一区| 香蕉成人伊视频在线观看| 黄色成人在线观看| 精品福利电影| 伊大人香蕉综合8在线视| 国产一区观看| 国语自产精品视频在线看抢先版结局| 牛夜精品久久久久久久99黑人 | 久久在线免费观看视频| 欧美专区在线观看一区| 欧美一区二区三区成人| 国产精品99久久久久久有的能看| 亚洲精品中文字| 欧美v日韩v国产v| 美腿丝袜亚洲色图| 亚洲欧美一区二区激情| 亚洲综合日韩| 国产精品性做久久久久久| 国产精品久久夜| 国产精品啊啊啊| 蜜桃精品一区二区三区| 亚洲一区中文| 蜜臀av一级做a爰片久久| 国产伦精品一区二区| 国产一区二区三区av电影| 一区二区三区亚洲| 亚洲国产色一区| 在线亚洲激情| 欧美一区二视频在线免费观看| 久久狠狠婷婷| 欧美在线一区二区| 欧美激情视频一区二区三区免费| 亚洲二区免费| 亚洲欧美日韩精品久久| 另类成人小视频在线| 欧美在线看片| 欧美另类在线观看| 久久久无码精品亚洲日韩按摩| 一本大道久久精品懂色aⅴ | 久久米奇亚洲| 欧美女人交a| 欧美精品久久久久久久| 国产欧美婷婷中文| 亚洲精品国产视频| 日韩一级精品视频在线观看| 性久久久久久久久| 久久狠狠婷婷| 亚洲第一精品福利| 亚洲欧洲一区二区三区在线观看| 亚洲第一精品影视| 亚洲视频专区在线| 欧美精品一区在线| 激情久久婷婷| 日韩亚洲一区在线播放| 久久国产综合精品| 99视频精品免费观看| 久久理论片午夜琪琪电影网| 欧美色区777第一页| 国产精品入口夜色视频大尺度| 国产亚洲精品一区二555| 国产亚洲精品美女| 亚洲一区制服诱惑| 欧美激情女人20p| 久久黄金**| 国产精品日韩一区二区三区| 亚洲美女av电影| 久久久欧美精品| 午夜亚洲一区| 国产精品久久久久久模特| 99精品欧美一区二区三区综合在线| 欧美一区永久视频免费观看| 99re热这里只有精品视频| 欧美成人乱码一区二区三区| 在线免费观看日韩欧美| 老巨人导航500精品| 性视频1819p久久| 国产欧美一区二区精品秋霞影院 | 久久gogo国模啪啪人体图| 99综合在线| 欧美日韩免费在线视频| 亚洲精品在线观看视频| 最新日韩欧美| 欧美激情视频免费观看| 亚洲国产成人av| 久久在线精品| 久久久99久久精品女同性 | 久久精品在这里| 在线视频观看日韩| 免费在线观看精品| 欧美mv日韩mv国产网站| 亚洲欧洲一区二区在线播放| 国产精品美女久久福利网站| 亚洲欧美在线高清| 亚洲一级黄色| 国产欧美日韩视频一区二区| 亚洲欧美日韩一区二区三区在线| 欧美在线网站| 久久久久久久久蜜桃| 伊人婷婷欧美激情| 欧美激情一区二区三区在线| 欧美成人日本| 欧美一级在线视频| 欧美中文字幕不卡| 在线观看日韩精品| 91久久国产精品91久久性色| 欧美日韩一区二区在线| 亚洲欧美日韩精品久久久久| 亚洲香蕉视频| 亚洲福利视频二区| 久久久免费精品| 欧美激情中文字幕在线| 午夜国产不卡在线观看视频| 久久国产精品高清| 亚洲精品黄色| 亚洲欧美日韩国产成人精品影院| 国产一区二区看久久| 亚洲精品国精品久久99热一| 国产精品yjizz| 欧美国产极速在线| 国产精品亚洲一区| 亚洲成色最大综合在线| 国产精品va在线播放我和闺蜜| 久久国产精品久久国产精品| 欧美人与禽猛交乱配| 亚洲一区二区三区四区中文| 欧美一区二区三区四区视频| 在线看国产日韩|