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