日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Item 16: 让const成员函数做到线程安全

發(fā)布時間:2025/4/14 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Item 16: 让const成员函数做到线程安全 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

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

博客已經(jīng)遷移到這里啦

如果我們在數(shù)學(xué)領(lǐng)域里工作,我們可能會發(fā)現(xiàn)用一個類來表示多項式會很方便。在這個類中,如果有一個函數(shù)能計算多選式的根(也就是,多項式等于0時,各個未知量的值)將變得很方便。這個函數(shù)不會改變多項式,所以很自然就想到把它聲明為const:

class Polynomial{ public:using RootsType = //一個存放多項式的根的數(shù)據(jù)結(jié)構(gòu)std::vector<double>; //(using的信息請看Item 9)...RootsType roots() const;...};

計算多項式的根代價可能很高,所以如果不必計算的話,我們就不想計算。如果我們必須要計算,那么我們肯定不想多次計算。因此,當(dāng)我們必須要計算的時候,我們將計算后的多項式的根緩存起來,并且讓roots函數(shù)返回緩存的根。這里給出最基本的方法:

class Polynomial{ public:using RootsType = std::vector<double>;RottsType roots() const{if(!rootsAreValid){ //如果緩存不可用... //計算根,把它們存在rootVals中rootsAreValid = true;}return rootVals;}private:mutable bool rootsAreValid{ false }; //初始化的信息看Item 7mutable RootsType rootVals{}; };

概念上來說,roots的操作不會改變Polynomial對象,但是,對于它的緩存行為來說,它可能需要修改rootVals和rootsAreValid。這就是mutable很經(jīng)典的使用情景,這也就是為什么這些成員變量的聲明帶有mutable。

現(xiàn)在想象一下有兩個線程同時調(diào)用同一個Polynomial對象的roots:

Polynomuial p;.../*-------- 線程1 -------- */ /*-------- 線程2 -------- */auto rootsOfP = p.roots(); auto valsGivingZero = p.roots();

客戶代碼是完全合理的,roots是const成員函數(shù),這就意味著,它表示一個讀操作。在多線程中非同步地執(zhí)行一個讀操作是安全的。至少客戶是這么假設(shè)的。但是在這種情況下,卻不是這樣,因為在roots中,這兩個線程中的一個或兩個都可能嘗試去修改成員變量rootsAreValid和rootVals。這意味著這段代碼在沒有同步的情況下,兩個不同的線程讀寫同一段內(nèi)存,這其實就是data race的定義。所以這段代碼會有未定義的行為。

現(xiàn)在的問題是roots被聲明為const,但是它卻不是線程安全的。這樣的const聲明在C++11和C++98中都是正確的(取多項式的根不會改變多項式的值),所以我們需要更正的地方是線程安全的缺失。

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

class Polynomial{ public:using RootsType = std::vector<double>;RottsType roots() const{std::lock_guard<std::mutex> g(m); //鎖上互斥鎖if(!rootsAreValid){ //如果緩存不可用... rootsAreValid = true;}return rootVals;} //解開互斥鎖private:mutable std::mutex m;mutable bool rootsAreValid{ false }; mutable RootsType rootVals{}; };

std::mutex m被聲明為mutable,因為對它加鎖和解鎖調(diào)用的都不是const成員函數(shù),在roots(一個const成員函數(shù))中,如果不這么聲明,m將被視為const對象。

值得注意的是,因為std::mutex是一個move-only類型(也就是,這個類型的對象只能move不能copy),所以把m添加到Polynomial中,會讓Polynomial失去copy的能力,但是它還是能被move的。

在一些情況下,一個mutex是負(fù)擔(dān)過重的。舉個例子,如果你想做的事情只是計算一個成員函數(shù)被調(diào)用了多少次,一個std::atomic計數(shù)器(也就是,其它的線程保證看著它的(counter的)操作不中斷地做完,看Item 40)常常是達(dá)到這個目的的更廉價的方式。(事實上是不是更廉價,依賴于你跑代碼的硬件和標(biāo)準(zhǔn)庫中mutex的實現(xiàn))這里給出怎么使用std::atomic來計算調(diào)用次數(shù)的例子:

class Point { public:...double distanceFromOrigin() const noexcept //noexcept的信息請看Item 14{++callCount; //原子操作的自增return std::sqrt((x * x) + (y * y));}private:mutable std::atomic<unsigned> callCount{ 0 }; };

和std::mutex相似,std::atomic也是move-only類型,所以由于callCount的存在,Point也是move-only的。

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

class Widget { public:...int magicValue() const{if (cacheValid) return cachedValue;else{auto val1 = expensiveComputation1();auto val2 = expensiveComputation2();cachedValue = val1 + val2; //恩,第一部分cacheValid = true; //恩,第二部分return cachedValue;}}private:mutable std::atomic<bool> cacheValid { false };mutable std::atomic<int> cachedValue; };

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

  • 一個線程調(diào)用Widget::magicValue,看到cacheValid是false的,執(zhí)行了兩個昂貴的計算,并且把它們的和賦給cachedValue。
  • 在這個時間點,第二個線程調(diào)用Widget::magicValue,也看到cacheValid是false的,因此同樣進(jìn)行了昂貴的計算(這個計算第一個線程已經(jīng)完成了)。(這個“第二個線程”事實上可能是一系列線程,也就會不斷地進(jìn)行這昂貴的計算)

這樣的行為和我們使用緩存的目的是相違背的。換一下cachedValue和CacheValid賦值的順序可以消除這個問題(不斷進(jìn)行重復(fù)計算),但是錯的更加離譜了:

class Widget { public:...int magicValue() const{if (cacheValid) return cachedValue;else{auto val1 = expensiveComputation1();auto val2 = expensiveComputation2();cacheValid = true; //恩,第一部分 return cachedValue = val1 + val2; //恩,第二部分 }}... };

想象一下cacheValid是false的情況:

  • 一個線程調(diào)用Widget::magicValue,并且剛執(zhí)行完:把cacheValid設(shè)置為true。
  • 同時,第二個線程調(diào)用Widget::magicValue,然后檢查cacheValid,發(fā)現(xiàn)它是true,然后,即使第一個線程還沒有把計算結(jié)果緩存下來,它還是直接返回cachedValue。因此,返回的值是不正確的。

讓我們吸取教訓(xùn)。對于單一的變量或者內(nèi)存單元,它們需要同步時,使用std::atomic就足夠了,但是一旦你需要處理兩個或更多的變量或內(nèi)存單元,并把它們視為一個整體,那么你就應(yīng)該使用mutex。對于Widget::magicValue,看起來應(yīng)該是這樣的:

class Widget { public:...int magicValue() const{std::lock_guard<std::mutex> guard(m); //鎖住mif (cacheValid) return cachedValue;else{auto val1 = expensiveComputation1();auto val2 = expensiveComputation2();cachedValue = val1 + val2; cacheValid = true; return cachedValue;}} //解鎖m...private:mutable std::mutex m;mutable int cachedValue; //不再是atomic了mutable bool cacheValid { false };};

現(xiàn)在,這個Item是基于“多線程可能同時執(zhí)行一個對象的const成員函數(shù)”的假設(shè)。如果你要寫一個const成員函數(shù),并且你能保證這里沒有多于一個的線程會執(zhí)行這個對象的cosnt成員函數(shù),那么函數(shù)的線程安全就不重要了。舉個例子,如果一個類的成員函數(shù)只是設(shè)計給單線程使用的,那么這個成員函數(shù)是不是線程安全就不重要了。在這種情況下,你能避免mutex和std::atomic造成的負(fù)擔(dān)。以及免受“包含它們的容器將變成move-only”的影響。然而,這樣的自由線程(threading-free)變得越來越不常見了,它們還將變得更加稀有。以后,const成員函數(shù)的多線程執(zhí)行一定會成為主題,這就是為什么你需要確保你的const成員函數(shù)是線程安全的。

            你要記住的事
  • 讓const成員函數(shù)做到線程安全,除非你確保它們永遠(yuǎn)不會用在多線程的環(huán)境下。
  • 比起mutex,使用std::atomic變量能提供更好的性能,但是它只適合處理單一的變量或內(nèi)存單元

轉(zhuǎn)載于:https://www.cnblogs.com/boydfd/p/5042876.html

總結(jié)

以上是生活随笔為你收集整理的Item 16: 让const成员函数做到线程安全的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。