Item 16 讓const成員函式做到執行緒安全

2022-07-16 23:33:23 字數 3252 閱讀 3246

本文翻譯自modern effective c++,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!

如果我們在數學領域裡工作,我們可能會發現用乙個類來表示多項式會很方便。在這個類中,如果有乙個函式能計算多選式的根(也就是,多項式等於0時,各個未知量的值)將變得很方便。這個函式不會改變多項式,所以很自然就想到把它宣告為const:

class polynomial;
計算多項式的根代價可能很高,所以如果不必計算的話,我們就不想計算。如果我們必須要計算,那麼我們肯定不想多次計算。因此,當我們必須要計算的時候,我們將計算後的多項式的根快取起來,並且讓roots函式返回快取的根。這裡給出最基本的方法:

class polynomial

return rootvals;

}private:

mutable bool rootsarevalid; //初始化的資訊看item 7

mutable rootstype rootvals{};

};

概念上來說,roots的操作不會改變polynomial物件,但是,對於它的快取行為來說,它可能需要修改rootvals和rootsarevalid。這就是mutable很經典的使用情景,這也就是為什麼這些成員變數的宣告帶有mutable。

現在想象一下有兩個執行緒同時呼叫同乙個polynomial物件的roots:

polynomuial p;

.../*-------- 執行緒1 -------- */ /*-------- 執行緒2 -------- */

auto rootsofp = p.roots(); auto valsgivingzero = p.roots();

客戶**是完全合理的,roots是const成員函式,這就意味著,它表示乙個讀操作。在多執行緒中非同步地執行乙個讀操作是安全的。至少客戶是這麼假設的。但是在這種情況下,卻不是這樣,因為在roots中,這兩個執行緒中的乙個或兩個都可能嘗試去修改成員變數rootsarevalid和rootvals。這意味著這段**在沒有同步的情況下,兩個不同的執行緒讀寫同一段記憶體,這其實就是data race的定義。所以這段**會有未定義的行為。

現在的問題是roots被宣告為const,但是它卻不是執行緒安全的。這樣的const宣告在c++11和c++98中都是正確的(取多項式的根不會改變多項式的值),所以我們需要更正的地方是執行緒安全的缺失。

解決這個問題最簡單的方式就是最常用的辦法:使用乙個mutex:

class polynomial

return rootvals;

} //解開互斥鎖

private:

mutable std::mutex m;

mutable bool rootsarevalid;

mutable rootstype rootvals{};

};

std::mutex m被宣告為mutable,因為對它加鎖和解鎖呼叫的都不是const成員函式,在roots(乙個const成員函式)中,如果不這麼宣告,m將被視為const物件。

值得注意的是,因為std::mutex是乙個move-only型別(也就是,這個型別的物件只能move不能copy),所以把m新增到polynomial中,會讓polynomial失去copy的能力,但是它還是能被move的。

在一些情況下,乙個mutex是負擔過重的。舉個例子,如果你想做的事情只是計算乙個成員函式被呼叫了多少次,乙個std::atomic計數器(也就是,其它的執行緒保證看著它的(counter的)操作不中斷地做完,看item 40)常常是達到這個目的的更廉價的方式。(事實上是不是更廉價,依賴於你跑**的硬體和標準庫中mutex的實現)這裡給出怎麼使用std::atomic來計算呼叫次數的例子:

class point 

private:

mutable std::atomiccallcount;

};

和std::mutex相似,std::atomic也是move-only型別,所以由於callcount的存在,point也是move-only的。

因為比起mutex的加鎖和解鎖,對std::atomic變數的操作常常更廉價,所以你可能會過度傾向於std::atomic。舉個例子,在乙個類中,快取乙個「計算昂貴」的int,你可能會嘗試使用一對std::atomic變數來代替乙個mutex:

class widget 

}private:

mutable std::atomiccachevalid ;

mutable std::atomiccachedvalue;

};

這能工作,但是有時候它會工作得很辛苦,考慮一下:

這樣的行為和我們使用快取的目的是相違背的。換一下cachedvalue和cachevalid賦值的順序可以消除這個問題(不斷進行重複計算),但是錯的更加離譜了:

class widget 

} ...

};

想象一下cachevalid是false的情況:

讓我們吸取教訓。對於單一的變數或者記憶體單元,它們需要同步時,使用std::atomic就足夠了,但是一旦你需要處理兩個或更多的變數或記憶體單元,並把它們視為乙個整體,那麼你就應該使用mutex。對於widget::magicvalue,看起來應該是這樣的:

class widget 

} //解鎖m

...private:

mutable std::mutex m;

mutable int cachedvalue; //不再是atomic了

mutable bool cachevalid ;

};

現在,這個item是基於「多執行緒可能同時執行乙個物件的const成員函式」的假設。如果你要寫乙個const成員函式,並且你能保證這裡沒有多於乙個的執行緒會執行這個物件的cosnt成員函式,那麼函式的執行緒安全就不重要了。舉個例子,如果乙個類的成員函式只是設計給單執行緒使用的,那麼這個成員函式是不是執行緒安全就不重要了。在這種情況下,你能避免mutex和std::atomic造成的負擔。以及免受「包含它們的容器將變成move-only」的影響。然而,這樣的自由執行緒(threading-free)變得越來越不常見了,它們還將變得更加稀有。以後,const成員函式的多執行緒執行一定會成為主題,這就是為什麼你需要確保你的const成員函式是執行緒安全的。

你要記住的事

讓Tab Bar的Item不被選中時不變灰色

為了讓tabbar的按鈕沒有被選中時,仍然保持原來的顏色 設定繪製模式,使不變灰 uitabbaritem onebaritem uitabbaritem alloc init onebaritem.image uiimage imagenamed news imagewithrenderingmo...

py程式設計技巧 1 6 如何讓字典保持有序

某程式設計競賽系統,對於參賽選手解題情況進行計時。選手完成題目後,就將該選手解題用時記錄到字典中,以便賽後按選手名次查詢成績 比賽結束後,需要按照排名次序依次列印選手成績,如何實現。d d jim 1,35 d leo 2,37 d bob 3,40 for k in d print k由結果可以發...