C++中原子变量的线程安全初始化
Thread-safe initialization of atomic variable in C++
请考虑以下 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));
}
};
你能帮我理解为什么后一个示例有效吗(是因为它不使用相同的互斥锁吗?有没有一种安全的方法可以在B
的初始值设定项列表中初始化id
,而无需将其锁定在 ctor 的主体中?我的要求是必须const
id
,并且id
的初始化在初始值设定项列表中进行。
首先,发布的代码中仍然存在基本的逻辑问题。您使用++ counter
作为id
。 考虑B
的第一次创造,在单个线程中。 B
将有id == 1
;push_back
之后 sharedResource
,您将有sharedResource.size() == 1
,并且只有访问它的法律索引才会0
。
此外,代码中存在明确的竞争条件。 即使你纠正上述问题(用counter ++
初始化id
(,假设counter
和sharedResource.size()
目前都在0
;您刚刚初始化。 线程一进入B
的构造函数,递增counter
,因此:
counter == 1
sharedResource.size() == 0
然后它被线程 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()
,并让它完成整个工作。
初始化对象时,它应该由单个线程拥有。然后,当它完成初始化时,它被共享。
如果存在线程安全初始化这样的事情,则意味着确保对象在初始化之前未被其他线程访问。
当然,我们可以讨论原子变量的线程安全assignment
。赋值不同于初始化。
您在子构造函数列表中初始化向量。这不是真正的原子操作。因此,在多线程系统中,您可能会同时受到两个线程的打击。这正在改变 id 是什么。欢迎来到线程安全 101!
将初始化移动到由锁包围的构造函数中,使得只有一个线程可以访问和设置向量。
解决此问题的另一种方法是将其移动到单格尔顿模式中。但是,每次获得对象时,您都需要为锁付费。
现在,您可以进入诸如双重检查锁定:)之类的事情
http://en.wikipedia.org/wiki/Double-checked_locking
- 如何将元素添加到数组的线程安全函数?
- C++中的线程安全删除
- 在std::thread中,joinable()然后join()线程安全吗
- 在c++队列中使用pop和visit实现线程安全
- 以线程安全的方式调用"QQuickPaintedItem::updateImage(const QImage&image)"(no QThread)
- 全局变量 多读取器 一个写入器多线程安全?
- 共享队列的线程安全
- boost::文件系统::recursive_directory_iterator多线程安全
- 以线程安全的方式转换 C/C++ 中时区名称字符串的时区偏移量
- 线程安全运算符<<
- 如何使缓存线程安全
- C++线程安全:如果只有一个线程可以写入非原子变量,但多个线程从中读取. 会遇到问题吗?
- 提升精神 V2 Qi 语法线程安全吗?
- asio 链对象线程安全吗?
- 线程安全队列 c++
- 提供对不同类型的数据(建议、代码审查)的线程安全访问的类
- 如何以线程安全的方式更改目录?
- 线程安全的引用计数队列C++
- 析构函数和线程安全
- 适用于大型数组的无复制线程安全环形缓冲区