請問,多個線程可以讀一個變數,只有一個線程可以對這個變數進行寫,到底要不要加鎖?
我自己的理解是,賦值操作對應兩條彙編指令,所以需要加鎖。不知道對不對
不光是原子性的問題。不加鎖的話,其他cpu讀到的可能還是cache裡面的值,而不是內存中已經被修改的值。這要看你的業務邏輯裡面的reader需要多快能夠看到writer的修改。
我不負責任地總結一句:凡是在網上問要不要加鎖的,答案一律是要加鎖,沒有例外。
- Benign data races: what could possibly go wrong?
- Ad Hoc Synchronization Considered Harmful
- How to miscompile programs with 「benign」 data races (https://hboehm.info/boehm-hotpar11.pdf)
為了避免扯皮(原子操作算不算加鎖?),第一句話的「加鎖」應該理解為「同步/synchronization」。其實我認為 atomic operations are dangerous,或者用 Herb Sutter 的話來說,是 Juggling Razor Blades,一般程序員老老實實用 mutex 就好了。
我知道會有很多人不同意我的觀點,而且會有各路專家站出來證明某種情況下不加鎖也是可行的,不必費力說服我,你們開心就好了。
必須要。
跟賦值是否是一條語句沒有關係,甚至看上去彙編是原子的操作(比如MOV [內存],寄存器這種)都不一定是真原子操作。
如果是多線程+多核的情況,你的第一個線程在CPU0上修改了這個變數,第二個線程不會馬上在CPU1上看到你的改動。
因為你操作的變數都在緩存(cache)中,CPU0/1可能會同時保存一份變數緩存,也就是說你的變數在緩存里是兩份,只有一個CPU(核)更新的話是不一定觸發另外一個CPU立即更新的。
具體可以參考這個問題:一個簡單的ia32的CPU指令重排序與cache問題,我的推算為什麼得不出示例的結果? - CPU 指令集
引用問題里的圖:
Java中最新的volatile是可以用的,C里的是不行的。Java中volatile會在x86平台上插入一個lock前綴(彙編指令)保證緩存一致性。
MESI協議能保證cache的一致性,但MESI無法保證以下的情況:
1、流水線上的load buffer;
2、跨cache行訪問;
基本都要,除了極少數的情況,譬如說使用intel的x86 cpu去改一個對齊了的、不會被優化掉的全局__int32變數。
如果你看不懂劉然的答案,那麼一定需要加鎖。
如果你看得懂劉然的答案,並且你確定各個線程的代碼都是你寫的(不存在你有個看不懂他的答案的隊友在和你合作的情況),請以他的答案為準。(1)看你這個變數的尺度。比如一個int變數,不需要加鎖。為什麼?因為它的尺度在數據匯流排上的傳送這個動作粒度足夠的小,本身就導致在並發條件下無法被更細分的切割。所以沒必要加鎖。當然,這並不意味著不存在臟讀的存在。只是意味著這時讀寫並發(可以存在任意多個對這個變數的讀寫線程)不會影響數據的【完整性】。(例如,當一個int類型的數據被從主存 load 到 cpu 內時,這個步驟是不會被其他的寫操作打斷的,store 到主存的時候也是如此。但請注意,當數據被 load 到 CPU 內部時,這時候其他線程是可能發生 store 過程的,換句話說,你讀到的數據,可能在數據進入到 CPU 的時候,已經被其他人改寫,此即所謂臟讀,如果你再以你臟讀的數據為基礎進行計算,然後去更新主存,即出現臟讀的副作用:更新覆蓋問題,這是更通用的多讀寫並發條件下,應用這一層必須考慮的)。
(2)這個變數的尺度比較大,導致它會被並發過程切割。那就要加鎖。比如你這個變數時一個較大的數組,較大的用戶對象。那麼,在一個線程對他進行寫操作的時候,完全有可能有其他線程同時來讀,這時候造成數據狀態的【不完整衝突】。所以,需要加鎖。主要是在寫的時候,另所有讀的請求等待,知道寫完成為止。當多個線程讀的時候,因為數據本身不變,所以是可以不加任何限制的並發的。如果這些讀線程之間,除了這個變數之外沒有別的任何直接或者間接的通信(比如說2個線程都在往一個tcp連接上發消息,可以認為他們通過tcp對端進行間接的通信),那麼這種「臟」讀是沒有任何問題的。
但是我不認為你能做出上面那樣的保證,所以還是推薦你兩眼一抹黑,鎖上吧。
當然需要,別聽前面那個case 1234分析的頭頭是道,需要就是需要。不需要的話你必須控制好所有在從代碼實現到CPU實際執行順序這一段的變數,你做得到嗎?
Benign data races: what could possibly go wrong?
本答案只做補充,除了其他人說的可能原因之外,還有 compiler (包括但不限於 C++ )關於程序行為的推斷也可能使安全的程序變為多線程不安全的程序。具體可以詳見某篇論文,我看過但找不到了。(詳見:How to miscompile programs with 「benign」 data races)
PS. 你知道 memory fence,那你知道 compiler fence 嗎?
PPS. 前面只是吐槽,補充一下:劉然的答案只說了CPU對於指令的執行,但實際代碼中的變數還有很多,從編程語言到彙編語言之間還有若干層,任何一層都可以在規則允許之上對代碼做出修改,從而導致「What You See Is Not What You Get」。
很多人誤解了一件事,不是修改才需要同步,讀的時候為了看到最新值,也需要同步
不是很同意 @北極 的答案,這個要分情況進行討論,其實大多數時候並不需要加鎖。
第一種情況:
讀線程不會干擾寫線程,換句話說,寫變數的線程下面的操作不依賴於讀線程已經看到自己所寫的值時:不需要加鎖,多核體系好比分散式系統,只有事件發生的因果順序才是重要的,絕對時間並不重要
第二種情況:
邏輯上允許讀線程讀到舊的值,此時不需要加鎖,這也是RCU的主要應用場景
第三種情況:
寫線程寫的數據處於同一條cache line內部,且可以原子的一條指令寫完,此時不需要加鎖,需要在讀寫兩側加上合適的Memory Barrier,在x86上是lock前綴指令和fence指令等
第四種情況:
請加讀寫鎖。
當然,如果你對性能要求十分苛刻,一個周期也不想浪費,可以試一下我提出的被動讀寫鎖
參見我的論文:Scalable Read-mostly Synchronization Using
Passive Reader-Writer Locks
https://www.usenix.org/system/files/conference/atc14/atc14-paper-liu.pdf
Cache一致性協議會保證小於word的讀寫因果一致性,前提是加volatile避免編譯器優化。
加鎖真的是一件很煩的事情。。。。哪天要是能夠無鎖編程。。。。就好了。。。。
或者要是能夠增加一個關鍵字 給對象 自動加鎖 倒也不失為一種好辦法.
簡化編程 是一條有前途的不歸路 。
正經答案:要加鎖。
提供一些搜索這方面相關資料關鍵字:
內存對齊,內存屏障,內存柵欄,acquire語義,release語義,互鎖,cpu指令亂序,MESI協議,std::atomic
順便說一句,標準c++裡面的volatile是沒有多線程可見性/一致性方面的語義的,只是保證每次從內存中重新讀取而不會因為被編譯器優化而讀取到了錯誤值,這個機制的目的是為了準確的讀取一些硬體映射到主存定址空間中的寄存器。但是MSVC確實通過互鎖操作做了額外的一致性保證。
要保證可見性啊。java中要用volatile。要不然別的線程看不到你的修改。
樓上不少大牛說必須鎖,我覺得不完全是這樣,這取決於你希望獲得怎樣的一致性。
不加鎖的優點是提高throughput。缺點在於,讀線程可能無法立即讀取最新的值。這個其實是某一種比較弱的一致性,叫做eventual consistency:只要所有線程"最終"都能讀到最新數據,不要求"立即"能讀到,依然可以被認為這是具有一致性的,只是比較弱而已。
所以具體加鎖不加鎖要看你的需求,如果你的計算是要求strong consistency (immediate consistency)那就加鎖吧;如果可以容忍eventual consistency,那不加鎖提高throughput也有可能是一種選擇。但考慮到cpu flush cache的時機等等複雜的問題,一般還是建議你加鎖,這樣更簡單一些。
這麼多答案,搞的我以前寫的代碼都不放心了。要不要加鎖啊
取決於你的需求。
比如線程A每次修改結構體里的一個變數,線程B讀的時候,可能讀到「半熟」信息。這個可能是不可接受的。所以必須加鎖。
但是如果線程A在後台(即線程B無感知)新建一個結構體,在修改完成後,替換掉原結構體的**指針**。這樣線程B讀到的就是「全熟」的信息。這樣就不用加鎖了。這個得看體系結構和內存模型。intel體系結構,如果變數小於一個指針的位元組數不用加鎖,硬體保證原子性
在當前的計算機體系結構上,ILP32或ILP64系統上int是可以不鎖的,LP64系統上是long可以不鎖,而且都要求地址是對齊的,當然在程序中要聲明為volatile防止編譯器優化…
取決於你希望怎麼樣的一致性
推薦閱讀:
※動不動就 32GB 以上內存的伺服器真需要關心內存碎片問題嗎?
※程序員的鄙視鏈是什麼?
※使用 Linux 的人一般是出於什麼原因選擇這個系統?
※為何 Linus 一個人就能寫出這麼強的系統,中國舉全國之力都做不出來?