push_back()
,也會導致core dump。解法一
加鎖是一種解決方案,比如互斥鎖std::mutex
。但是加std::mutex
確實性能較差。對于多讀少寫的場景可以用讀寫鎖(也叫共享獨占鎖)來緩解。比如C 17引入了std::shared_mutex
。更多鎖的種類可以閱讀之前寫的這篇文章:如何理解互斥鎖、條件變量、讀寫鎖以及自旋鎖?當然本文的目的自然不是自我重復再次介紹一次鎖的使用,請繼續(xù)閱讀解法二!解法二
更多的時候,其實可以通過固定vector的大小,避免動態(tài)擴容(無push_back)來做到lock-free!即在開始并發(fā)讀寫之前(比如初始化)的時候,給vector設置好大小。struct?Data?{
...
};
vector?v;
v.resize(1000);
注意是resize()
,不是reserve()
!可能大家平時用reserve()
比較多,顧名思義,reserve就是預留內存。為的是避免內存重新申請以及容器內對象的拷貝。說白了,reserve()
是給push_back()
準備的!而resize除了預留內存以外,還會調用容器元素的構造函數(shù),不僅分配了N個對象的內存,還會構造N個對象。從這個層面上來說,resize()
在時間效率上是比reserve()
低的。但是在多線程的場景下,用resize再合適不過。你可以resize好N個對象,多線程不管是讀還是寫,都是通過容器的下標訪問operator[]
來訪問元素,不要push_back()
新元素。所謂的『寫操作』在這里不是插入新元素,而是修改舊元素。如果N的最大個數(shù)是可以預期的就直接設置就好,如果沒辦法預期就再把vector搞成ring buffer(環(huán)形隊列)來緩解壓力。可以給元素類加上成員變量標記當前的讀寫狀態(tài)、是否被消費等等。當然,你會說,如果B,C,D,E,F(xiàn)這個5個線程是等價的,要不停消費vector中的元素,會造成重復消費不?當然會。你可以把隊列頭的下標定義成原子變量(std::atomic
),盡管原子變量也需要做線程同步,但是比一般的鎖開銷要小很多啦。如果你想連原子變量也不用,有沒有辦法呢?有啊。那就給B,C,D,E,F(xiàn)分配不同的消費隊列啊。比如當前有5個讀線程,那么每個線程就消費下標對5取模之后的某個固定結果的下標。比如:- B消費:0、5、10、15、……
- C消費:1、6、11、16、……
- D消費:2、7、12、17、……
- E消費:3、8、13、18、……
- F消費:4、9、14、19、……
分段加鎖
,減少一點鎖沖突的概率,或者用一下CAS
的策略。另外對于unordered_map,在單寫多讀的多線程場景下,會不會有問題呢?也可能有。gcc 4.7.2的unordered_map實現(xiàn)曾被爆出有這個問題。原因的新插入的元素,觸發(fā)了rehash,讓其他線程在unordered_map中查找的過程之中,出現(xiàn)了core dump。見:https://stackoverflow.com/questions/16353334/segv-in-gccs-stdunordered-map我不確定clang以及后續(xù)的gcc版本是否還有此問題。應該在不添加任何額外同步代碼的情況下,無法解決。
容器并發(fā)前初始化與偽共享的爭議
本文內容曾經(jīng)在知乎上寫過,有網(wǎng)友評論:解法二會有false sharing(偽共享)的問題。這里簡單回應一下,談論偽共享,要考慮具體的場景。的確某些時候偽共享會帶來性能損失,但是要和并行化帶來的性能提升來比較,孰高孰低。如果并行提升的性能足夠多,是足以彌補這點偽共享的損失的。比如我要進行遠程IO,我有N個key要查詢redis,把他們的結果存儲到一個vector中,這個vector的寫入操作在IO的異步回調函數(shù)中。在不加任何額外處理的情況下,極大概率會導致vector的core dump。而如果vector初始化一下,則無需在回調函數(shù)中加鎖,就能保證安全。這時候并行IO本身帶來的性能提升,遠遠大于可能
的偽共享帶來損失。這里為什么說可能
呢?因為偽共享的觸發(fā)沒你想象的這么簡單。如何成功模擬出一次偽共享帶來性能損失的例子?你可以寫程序自測一下,并不容易……甚至你改一下優(yōu)化級別,改成O2,測試表現(xiàn)都很不一樣。一般網(wǎng)絡上談論偽共享時所舉的例子,并不是一個vector中多個元素之間并行讀寫觸發(fā)了偽共享。而是vector的元素類型是一個對象,對象中有2個數(shù)據(jù)字段a和b,在多線程分別更新同一個元素的a和b字段的時候,導致了偽共享。比如一個線程更新vector中每個元素的a字段,另外一個線程更新vector中每個元素的b字段。Anyway,偽共享的議題比較復雜,歡迎留意評論!- EOF -