我有一个
optional<T>
,它由单个线程“最终”填充一次 - 在它进行一些计算、IO 等之后。有很多读者调用 has_value()
,如果为 true,则处理 T
值。我不需要为此使用互斥锁和锁,因为它只能设置一次,所以我只需要一个atomic_bool
。但这似乎很浪费1 - 毕竟,std::optional
使用额外的bool
来存储是否有值。我可以重新实现 optional
但使 bool
原子化,但这是一个非常粗糙的解决方案。
显示设置一次/永不重置原子选项的原子版本的简化示例: https://godbolt.org/z/YTaf878rK
template <typename T>
struct atomic_optional
{
void set(T&& t)
{
opt = std::move(t);
// must set after setting the value so it synchronizes with
// another thread calling has_value() in the correct order
isSet = true;
}
// must call before get() or contract broken
bool has_value()
{
// if this is not atomic, there is no guarantee that all threads
// can see that opt.has_value() has changed
return isSet;
}
// can only call IFF has_value() returns true
const T& get()
{
return *opt;
}
std::optional<T> opt {};
std::atomic_bool isSet {false};
};
我可以使用
std::atomic_thread_fence
而不是 atomic_bool
吗?例如。 fence 而不是将 isSet
设置为 true,以及在调用 has_value()
之前在 opt.has_value()
中进行栅栏。 docs 说你可以同步 2 个栅栏以进行非原子操作,但它还特别提到了一个原子对象(我这里没有)。如果栅栏可以作为这里的解决方案,那么在两个栅栏上使用 memory_order_seq_cst
是正确的,对吗?如果在某些架构上有点慢 - 获取和释放语义非常微妙,很容易被代码搅动破坏,并且难以重现(尤其是在 x86/x86-64 上[不可能如此?])。
Fence 版本 - 这是正确的吗?我可以在这里使用其他更适合的实用程序吗? https://godbolt.org/z/baeacMhf5
template <typename T>
struct atomic_optional
{
void set(T&& t)
{
opt = std::move(t);
std::atomic_thread_fence(std::memory_order_seq_cst); // memory_order_release?
}
bool has_value()
{
std::atomic_thread_fence(std::memory_order_seq_cst); // memory_order_acquire?
return opt.has_value();
// return value is immediately out of date if false and callers are expected
// to understand this e.g. call again after doing some work
}
const T& get()
{
return *opt;
}
std::optional<T> opt {};
};
1 对于浪费的一些定义 - 我意识到这个问题主要是学术性的。在这种情况下,它很可能可以忽略不计,因为
optional
添加了 bool
,然后很可能需要填充字节。在很多情况下,“额外”的 atomic_bool
将适合这些填充字节,因此不太可能实际增加对象的大小。对于复合对象(这是其成员,并且具有其他 1 字节或 2 字节成员),它可能会节省一些字节并实际上防止该对象需要填充。还要考虑 markable,它是 optional
的替代方案,不会添加额外的 bool
,而是使用哨兵值。
不,您不能使用栅栏将由于使用非原子变量而导致线程不安全的代码转换为线程安全代码。
当布尔标志不是原子的时,可能会出现数据竞争,这意味着一个线程设置该标志,另一个线程读取该标志,并且这两个操作都不会在另一个操作之前发生。
通常,互斥锁用于防止访问非原子变量时出现数据竞争。实际上,如果两个线程尝试同时锁定互斥体,其中一个线程会被迫等待,直到另一个线程释放互斥体,此时该线程已经完成对变量的访问,从而“减慢”速度。因此,其中一个访问发生在另一个访问之前(取决于哪个线程首先成功锁定互斥体)。
fence的作用是在某些情况下引入线程之间的同步。非正式地,这意味着在某些情况下,到达栅栏的线程可以保证观察到程序的状态,这样由较早到达栅栏的其他线程引起的所有副作用都已经完成。但是,在任何情况下,到达栅栏的线程都不会等待其他线程先到达栅栏!因此,在您的代码中,当一个线程到达
has_value
中的栅栏时,它不会等待。然后它继续调用 opt.has_value()
;与此同时,其他一些线程可能已经开始写入opt
。所以数据竞争仍然存在。栅栏不能替代互斥锁。
您需要一个互斥体或原子变量。