我有一個全域參考計數物件obj,我想通過使用原子操作來防止資料競爭:
T* obj; // initially nullptr
std::atomic<int> count; // initially zero
我的理解是,我需要std::memory_order_release在寫入后使用obj,以便其他執行緒知道它正在創建:
void increment()
{
if (count.load(std::memory_order_relaxed) == 0)
obj = std::make_unique<T>();
count.fetch_add(1, std::memory_order_release);
}
同樣,我需要std::memory_order_acquire在讀取計數器時使用,以確保執行緒具有obj被更改的可見性:
void decrement()
{
count.fetch_sub(1, std::memory_order_relaxed);
if (count.load(std::memory_order_acquire) == 0)
obj.reset();
}
我不相信上面的代碼是正確的,但我不完全確定為什么。我覺得在obj.reset()被呼叫之后,應該有一個std::memory_order_release操作來通知其他執行緒。那是對的嗎?
是否還有其他可能出錯的事情,或者在這種情況下我對原子操作的理解完全錯誤?
uj5u.com熱心網友回復:
無論記憶體順序如何,它都是錯誤的。
正如@MaartenBamelis 指出的那樣,increment物件的并發呼叫被構造了兩次。concurrent 也是如此decrement:物件被重置兩次(這可能導致雙重解構式呼叫)。
T* obj;請注意,宣告和使用它之間存在分歧,unique_ptr但原始指標和唯一指標都不能安全地進行并發修改。在實踐中,resetordelete會檢查指標是否為空,然后洗掉并將其設定為空,而這些步驟不是原子的。
fetch_add并且fetch_sub是 fetch 和 op 而不僅僅是 op 是有原因的:如果你不使用在操作程序中觀察到的值,它很可能是一場比賽。
uj5u.com熱心網友回復:
這段代碼本質上是亂七八糟的。如果兩個執行緒increment在count初始時同時呼叫0,兩者都將看到countas 0,并且都將創建obj(并競相查看保留哪個副本;給定unique_ptr沒有特殊的執行緒保護,如果其中兩個同時設定它,可能會發生可怕的事情)。
如果兩個執行緒decrement同時(持有最后兩個參考),并完成fetch_sub之前的任一呼叫load,則兩者都會reset obj(也很糟糕)。
如果 adecrement完成了fetch_sub(to ),那么在發生之前0另一個執行緒increments將看到as并重新初始化。物件是在被替換后被清除,還是在被清除后被替換,或者兩者的某種可怕的混合,將取決于's是在' s之前還是之后運行。decrement loadincrementcount0incrementfetch_adddecrementload
簡而言之:如果您發現自己對同一個變數使用了兩個單獨的原子操作,并測驗其中一個的結果(沒有回圈,如比較和交換回圈),那么您就錯了。
更正確的代碼如下所示:
void increment() // Still not safe
{
// acquire is good for the != 0 case, for a later read of obj
// or would be if the other writer did a release *after* constructing an obj
if (count.fetch_add(1, std::memory_order_acquire) == 0)
obj = std::make_unique<T>();
}
void decrement()
{
if (count.fetch_sub(1, std::memory_order_acquire) == 1)
obj.reset();
}
但即便如此,它也不可靠;不能保證,when countis ,兩個0執行緒不能同時呼叫它們,雖然它們中的一個可以保證看到as ,但說-seeing 執行緒可能最終會延遲,而將其視為假設的執行緒可能會延遲該物件存在并在初始化之前使用它。incrementfetch_addcount001
我不會發誓這里沒有無互斥鎖的解決方案,但處理與原子有關的問題幾乎肯定不值得頭疼。
可以將互斥鎖限制在if()分支內部,但采用互斥鎖也是一個原子 RMW 操作(對于良好的輕量級實作而言,這并不多),因此這不一定有很大幫助。如果你需要非常好的讀取端縮放,你會想要研究像RCU而不是 ref-count 的東西,讓讀者真正只讀,而不是與其他讀者競爭。
uj5u.com熱心網友回復:
我真的沒有看到用原子實作參考計數資源的簡單方法。也許有一些我還沒有想到的聰明方法,但根據我的經驗,聰明不等于可讀。
我的建議是首先使用互斥鎖來實作它。然后你只需鎖定互斥體,檢查參考計數,做任何需要做的事情,然后再次解鎖。保證正確:
std::mutex mutex;
int count;
std::unique_ptr<T> obj;
void increment()
{
auto lock = std::scoped_lock{mutex};
if ( count == 1) // Am I the first reference?
obj = std::make_unique<T>();
}
void decrement()
{
auto lock = std::scoped_lock{mutex};
if (--count == 0) // Was I the last reference?
obj.reset();
}
雖然在這一點上,我只會使用 astd::shared_ptr而不是自己管理參考計數:
std::mutex mutex;
std::weak_ptr<T> obj;
std::shared_ptr<T> acquire()
{
auto lock = std::scoped_lock{mutex};
auto sp = obj.lock();
if (!sp)
obj = sp = std::make_shared<T>();
return sp;
}
我相信這也使得在構造物件時可能引發例外時安全。
互斥體的性能令人驚訝,所以我希望鎖定代碼非常快,除非你有一個高度專業化的用例,你需要代碼是無鎖的。
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/424568.html
