C++ std::memory_order_relaxed 和 skip/stop 标志
C++ std::memory_order_relaxed and skip/stop flag
是否可以使用 std::memory_order_relaxed
作为跳过标志,如 iterate
:
constexpr static const std::size_t capacity = 128;
std::atomic<bool> aliveness[capacity];
T data[capacity]; // immutable/atomic/etc.
template<class Closure>
void iterate(Closure&& closure){
for(std::size_t i = 0; i<capacity; i++){
if (!aliveness[i].load(std::memory_order_relaxed)) continue;
closure( data[i] );
}
}
void erase(std::size_t index){
aliveness[index].store(false, std::memory_order_relaxed);
}
或者我应该改用 release/acquire?
aliveness[i]
可能又变成了"alive"。
iterate
和 erase
从多个线程同时调用。考虑 data
immutable/atomic/synchronized 在其他一些外部锁下。
假设:当您使用 erase
时,其他线程可以随时 运行 iterate()
。 (问题的早期版本没有指定不可变。如果更新 data[i]
的锁定(或缺少锁定)未按顺序写入 alive[i]
,则此答案仍然相关。写入 alive[i]
。)
如果数据确实是不可变的,那么 mo_relaxed
肯定没问题,除非您需要这些商店的全局可见性,以便根据某些东西进行排序 else 线程是在做。 mo_relaxed
存储最终总是对其他线程可见(并且在当前的 CPU 上,会很快)。
如果要在 alive[i]
为 false 时修改非原子 data[i]
,则需要确保其他线程在修改时不使用它的值。那将是 C++ 中的 UB,以及实际硬件上的实际正确性问题,具体取决于 T
和 closure
.
获取语义将适用于 iterate
。对 data[i]
的访问逻辑上发生在 alive[i]
之后,因为单向障碍的方向正确。 The acquire-load won't reorder with later loads or stores, only earlier ones.
但是erase
的店铺是有问题的。在对 data[i]
进行任何修改之前,它需要全局可见。但是允许发布商店与以后的商店重新订购。你需要的是 a release fence to block reordering of stores in both directions.
void erase(std::size_t index){
aliveness[index].store(false, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_release);
// or don't do that, this probably wouldn't be enough synchronization
// and whatever you do that syncs the data might make this unnecessary.
}
如果 T
是原子类型,那么 data[i]
的发布存储就可以了。但是不要那样做;如果 T
太大而不能成为无锁原子,那会很糟糕。
(更新:我不确定这是否完全有效。ISO C++ 内存排序规则是根据 synchronizes-with / happens-before 以及什么值定义的允许查看给定的负载。不是在原子操作和非原子操作之间重新排序本身。
此外,这就像您为 SeqLock 所做的一样,但这也取决于读取的数据争用 UB。如果另一个线程要读取 data[i]
,如果它检查 alive[i]
然后读取,就会出现 TOCTOU 竞争:我们对 alive
的写入可能发生在该读取之后,然后我们对 data[i]
可能与另一个线程读取同时发生。
所以这可能不足以擦除/修改/取消擦除元素,即使在像 x86 这样的强顺序非怪异机器上也是如此。)
在某些实现中,seq-cst 存储也可以,但我认为仅作为实现细节。它通常会产生一个存储 + 一个全屏障 asm 指令。 (例如 x86 MFENCE)。所以它起作用只是因为编译器将 seq-cst 存储实现为存储 + thread_fence(seq_cst)
。在 AArch64 上情况并非如此,其中 STLR 只是一个释放操作;只有与 LDAR 的交互才能给出 SC-DRF。 (seq_cst 用于无数据竞争的程序)。
请注意,如果 closure
修改 data[]
,则 iterate
不安全,除非一次只能有一个线程调用它。在这种情况下,这样做的意义何在?所以你可能应该使用
void iterate(Closure&& closure) const
{ ... }
所以 iterate
仅适用于容器的 const
个对象。
是否可以使用 std::memory_order_relaxed
作为跳过标志,如 iterate
:
constexpr static const std::size_t capacity = 128;
std::atomic<bool> aliveness[capacity];
T data[capacity]; // immutable/atomic/etc.
template<class Closure>
void iterate(Closure&& closure){
for(std::size_t i = 0; i<capacity; i++){
if (!aliveness[i].load(std::memory_order_relaxed)) continue;
closure( data[i] );
}
}
void erase(std::size_t index){
aliveness[index].store(false, std::memory_order_relaxed);
}
或者我应该改用 release/acquire?
aliveness[i]
可能又变成了"alive"。
iterate
和 erase
从多个线程同时调用。考虑 data
immutable/atomic/synchronized 在其他一些外部锁下。
假设:当您使用 erase
时,其他线程可以随时 运行 iterate()
。 (问题的早期版本没有指定不可变。如果更新 data[i]
的锁定(或缺少锁定)未按顺序写入 alive[i]
,则此答案仍然相关。写入 alive[i]
。)
如果数据确实是不可变的,那么 mo_relaxed
肯定没问题,除非您需要这些商店的全局可见性,以便根据某些东西进行排序 else 线程是在做。 mo_relaxed
存储最终总是对其他线程可见(并且在当前的 CPU 上,会很快)。
如果要在 alive[i]
为 false 时修改非原子 data[i]
,则需要确保其他线程在修改时不使用它的值。那将是 C++ 中的 UB,以及实际硬件上的实际正确性问题,具体取决于 T
和 closure
.
获取语义将适用于 iterate
。对 data[i]
的访问逻辑上发生在 alive[i]
之后,因为单向障碍的方向正确。 The acquire-load won't reorder with later loads or stores, only earlier ones.
但是erase
的店铺是有问题的。在对 data[i]
进行任何修改之前,它需要全局可见。但是允许发布商店与以后的商店重新订购。你需要的是 a release fence to block reordering of stores in both directions.
void erase(std::size_t index){
aliveness[index].store(false, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_release);
// or don't do that, this probably wouldn't be enough synchronization
// and whatever you do that syncs the data might make this unnecessary.
}
如果 T
是原子类型,那么 data[i]
的发布存储就可以了。但是不要那样做;如果 T
太大而不能成为无锁原子,那会很糟糕。
(更新:我不确定这是否完全有效。ISO C++ 内存排序规则是根据 synchronizes-with / happens-before 以及什么值定义的允许查看给定的负载。不是在原子操作和非原子操作之间重新排序本身。
此外,这就像您为 SeqLock 所做的一样,但这也取决于读取的数据争用 UB。如果另一个线程要读取 data[i]
,如果它检查 alive[i]
然后读取,就会出现 TOCTOU 竞争:我们对 alive
的写入可能发生在该读取之后,然后我们对 data[i]
可能与另一个线程读取同时发生。
所以这可能不足以擦除/修改/取消擦除元素,即使在像 x86 这样的强顺序非怪异机器上也是如此。)
在某些实现中,seq-cst 存储也可以,但我认为仅作为实现细节。它通常会产生一个存储 + 一个全屏障 asm 指令。 (例如 x86 MFENCE)。所以它起作用只是因为编译器将 seq-cst 存储实现为存储 + thread_fence(seq_cst)
。在 AArch64 上情况并非如此,其中 STLR 只是一个释放操作;只有与 LDAR 的交互才能给出 SC-DRF。 (seq_cst 用于无数据竞争的程序)。
请注意,如果 closure
修改 data[]
,则 iterate
不安全,除非一次只能有一个线程调用它。在这种情况下,这样做的意义何在?所以你可能应该使用
void iterate(Closure&& closure) const
{ ... }
所以 iterate
仅适用于容器的 const
个对象。