考虑以下C ++ 11代码,其中类B
被实例化并由多个线程使用。因为B
修改了共享向量,所以我必须在B
的ctor和成员函数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));
}
};
你能帮我理解为什么后一个例子有效(是因为它不使用相同的互斥体?)?有没有一种安全的方法可以在id
的初始化列表中初始化B
而不将其锁定在ctor的主体中?我的要求是id
必须为const
,并且id
的初始化发生在初始化列表中。
答案 0 :(得分:2)
首先,发布的代码中仍然存在一个基本的逻辑问题。
您使用++ counter
作为id
。考虑B
的第一次创建,
在一个线程中。 B
将id == 1
;在push_back
之后
sharedResource
,您将拥有sharedResource.size() == 1
,以及0
只有访问它的合法索引才是id
。
此外,代码中存在明显的竞争条件。即使你
请更正上述问题(使用counter ++
初始化counter
)
sharedResource.size()
和0
目前都是B
;
你刚刚初始化了。线程1进入counter
的构造函数,
增加counter == 1
sharedResource.size() == 0
,所以:
counter
然后它被线程2中断(在它获取互斥锁之前)
也会增加id
(到2),并使用其先前的值(1)作为
push_back
。然而,在线程2中的sharedResource.size() == 1
之后,我们只有
counter
,唯一的合法索引是0。
在实践中,我会避免两个单独的变量(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 );
// ...
}
};
const:
sharedResource
(请注意,这需要两次获取互斥锁。或者,您
可以传递完成更新所需的其他信息
getNewId()
到{{1}},让它完成整个工作。)
答案 1 :(得分:1)
当一个对象被初始化时,它应该由一个线程拥有。然后,当它被初始化时,它被共享。
如果存在线程安全初始化这样的事情,则意味着确保在初始化之前其他线程无法访问该对象。
当然,我们可以讨论原子变量的线程安全assignment
。分配与初始化不同。
答案 2 :(得分:0)
您在初始化向量的子构造函数列表中。这不是真正的原子操作。因此在多线程系统中,您可能会同时受到两个线程的攻击。这正在改变id是什么。欢迎来到安全线101!
将初始化移动到由锁定所包围的构造函数中,这样只有一个线程可以访问并设置向量。
解决这个问题的另一种方法是将其转变为singelton模式。但是每当你拿到物体时,你就会为锁付钱。
现在你可以进入像双重检查锁定的东西:)