首先是:x86彙編中,對任何記憶體地址中的1byte的讀永遠是原子的.也就是說對一個char的讀取永遠是原子的,對記憶體地址對齊2byte的int16型別的讀取是原子的,對4byte對齊的int32型別讀取是原子的,從從奔騰開始,對8byte對齊地址的int64讀取是原子的.所以如果你用的是彙編,保證這些就行了.但C/C++中又是另一番情景:C/C++中,編譯器保證基礎型別的記憶體對齊,例如保證double型別的對齊是8(或者4,忘了),即使是malloc出來的也可以保證對齊.但是由於各種不可避免的指標轉換,例如 char a[4],float* p=(float*)a的存在,使得對齊的保證基本名存實亡.而且,當一個比較長的型別,例如double被編譯器放入暫存器的時候,C++標準根本不保證只用一條指令就將它放入一個暫存器中.例如我可以先把前半部分放入eax,等一會兒再把後半部分放入edx等等.不過,如果你能夠確保對齊,那麼大多數情況下雖然UB,但你的程式碼還是有可能正常工作的.再然後,其實上面說的根本不用考慮,因為在C/C++標準中,一個變數除了使用atomic相關的函式以外,任何多執行緒同時進行的讀寫實際上都是UB.所以,除非使用標準中的atomic功能,或者使用編譯器自帶的一些擴充套件,例如InterlockedAdd之類的,否則都是bug的隱患.例如,有非常多的開O2以上最佳化就出錯的多執行緒相關程式碼就是由於類似的原因導致的.一個很經典的例子就是一個網上流傳的很廣的C++的單例類,以下是那段程式碼:
首先是:x86彙編中,對任何記憶體地址中的1byte的讀永遠是原子的.也就是說對一個char的讀取永遠是原子的,對記憶體地址對齊2byte的int16型別的讀取是原子的,對4byte對齊的int32型別讀取是原子的,從從奔騰開始,對8byte對齊地址的int64讀取是原子的.所以如果你用的是彙編,保證這些就行了.但C/C++中又是另一番情景:C/C++中,編譯器保證基礎型別的記憶體對齊,例如保證double型別的對齊是8(或者4,忘了),即使是malloc出來的也可以保證對齊.但是由於各種不可避免的指標轉換,例如 char a[4],float* p=(float*)a的存在,使得對齊的保證基本名存實亡.而且,當一個比較長的型別,例如double被編譯器放入暫存器的時候,C++標準根本不保證只用一條指令就將它放入一個暫存器中.例如我可以先把前半部分放入eax,等一會兒再把後半部分放入edx等等.不過,如果你能夠確保對齊,那麼大多數情況下雖然UB,但你的程式碼還是有可能正常工作的.再然後,其實上面說的根本不用考慮,因為在C/C++標準中,一個變數除了使用atomic相關的函式以外,任何多執行緒同時進行的讀寫實際上都是UB.所以,除非使用標準中的atomic功能,或者使用編譯器自帶的一些擴充套件,例如InterlockedAdd之類的,否則都是bug的隱患.例如,有非常多的開O2以上最佳化就出錯的多執行緒相關程式碼就是由於類似的原因導致的.一個很經典的例子就是一個網上流傳的很廣的C++的單例類,以下是那段程式碼:
這個雙檢鎖的程式碼很可能不能正常工作,因為首先是編寫者沒有告知編譯器必須假設instance是可能被其他執行緒改變的,因此編譯器完全可以認為兩次if只保留一個就行了(當然也可能不會).因此首先instance必須改為volatile的,然後就是上面所說的原子性,instance應該改為atomic<Singleton*>.C/C++中變數的原子性其實是個巨大的坑,C++11和C11之前對多執行緒的問題幾乎隻字不提,也沒有語言層面對原子性的保證,(上文中那段單例的程式碼應該也是C11之前出現的).所以程式設計師也沒有更好的辦法,只能使用GCC和VC裡自帶的那堆原子操作,或者懶了就直接不考慮這問題了.因此只能寫這種有隱含問題的程式碼,現在沒問題了,大膽用atomic<>吧.