[英]Thread-safe initialization of atomic variable in C++
考慮以下 C++11 代碼,其中 class B
被實例化並由多個線程使用。 因為B
修改了共享向量,所以我必須在B
的構造函數和成員 function foo 中鎖定對它的訪問。 為了初始化成員變量id
,我使用了一個計數器,它是一個原子變量,因為我從多個線程訪問它。
struct A {
A(size_t id, std::string const& sig) : id{id}, signature{sig} {}
private:
size_t id;
std::string signature;
};
namespace N {
std::atomic<size_t> counter{0};
typedef std::vector<A> As;
std::vector<As> sharedResource;
std::mutex barrier;
struct B {
B() : id(++counter) {
std::lock_guard<std::mutex> lock(barrier);
sharedResource.push_back(As{});
sharedResource[id].push_back(A("B()", id));
}
void foo() {
std::lock_guard<std::mutex> lock(barrier);
sharedResource[id].push_back(A("foo()", id));
}
private:
const size_t id;
};
}
不幸的是,這段代碼包含競爭條件並且不能像這樣工作(有時 ctor 和 foo() 不使用相同的 id)。 如果我將 id 的初始化移動到被互斥鎖鎖定的 ctor 主體,它會起作用:
struct B {
B() {
std::lock_guard<std::mutex> lock(barrier);
id = ++counter; // counter does not have to be an atomic variable and id cannot be const anymore
sharedResource.push_back(As{});
sharedResource[id].push_back(A("B()", id));
}
};
你能幫我理解為什么后一個例子有效嗎(是因為它沒有使用相同的互斥量嗎?)? 有沒有一種安全的方法可以在B
的初始化列表中初始化id
而無需將其鎖定在 ctor 的主體中? 我的要求是id
必須是const
並且id
的初始化發生在初始化列表中。
首先,發布的代碼中仍然存在一個基本的邏輯問題。 您使用++ counter
作為id
。 考慮在單個線程中首次創建B
。 B
將有id == 1
; 在sharedResource
的push_back
之后,您將擁有sharedResource.size() == 1
,並且訪問它的唯一合法索引將是0
。
此外,代碼中存在明顯的競爭條件。 即使您更正了上述問題(使用counter ++
初始化id
),假設counter
和sharedResource.size()
當前均為0
; 你剛剛初始化。 線程一進入B
的構造函數,遞增counter
,因此:
counter == 1
sharedResource.size() == 0
然后它被線程 2 中斷(在它獲取互斥鎖之前),線程 2 也將counter
遞增(到 2),並將其先前的值 (1) 用作id
。 然而,在線程 2 中的push_back
之后,我們只有sharedResource.size() == 1
,唯一合法的索引是 0。
實際上,我會避免兩個單獨的變量( counter
和sharedResource.size()
),它們應該具有相同的值。 根據經驗:應該相同的兩件事不會相同——唯一應該使用冗余信息的時間是在將其用於控制時; 即在某些時候,您有一個assert( id == sharedResource.size() )
或類似的東西。 我會使用類似的東西:
B::B()
{
std::lock_guard<std::mutex> lock( barrier );
id = sharedResource.size();
sharedResource.push_back( As() );
// ...
}
或者如果你想使id
常量:
struct B
{
static int getNewId()
{
std::lock_guard<std::mutex> lock( barrier );
int results = sharedResource.size();
sharedResource.push_back( As() );
return results;
}
B::B() : id( getNewId() )
{
std::lock_guard<std::mutex> lock( barrier );
// ...
}
};
(請注意,這需要獲取互斥量兩次。或者,您可以將完成更新sharedResource
所需的附加信息傳遞給getNewId()
,並讓它完成整個工作。)
初始化 object 時,它應該由單個線程擁有。 然后當它完成初始化時,它就被共享了。
如果存在線程安全初始化這樣的事情,則意味着確保 object 在初始化之前沒有變得可供其他線程訪問。
當然,我們可以討論原子變量的線程安全assignment
。 賦值不同於初始化。
您在初始化向量的子構造函數列表中。 這不是真正的原子操作。 所以在多線程系統中,你可能會同時被兩個線程擊中。 這正在改變 id 是什么。 歡迎來到線程安全 101!
將初始化移動到被鎖包圍的構造函數中,這樣只有一個線程可以訪問和設置向量。
解決此問題的另一種方法是將其移動到單例模式中。 但是,每次獲得 object 時,您都需要為鎖付費。
現在你可以進入雙重檢查鎖定之類的東西了:)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.