我有一个分配器,它可以轻松地跟踪当前分配的字节数。它们只是加减,所以除了确保修改是原子的之外,我不需要线程之间的任何同步。
但是,我偶尔希望检查分配的字节数(例如,关闭程序时),并且希望确保任何挂起的写入都已提交。我假设在这种情况下,我需要一个完整的内存屏障,以防止任何先前的写入在屏障之后被移动,并防止下一个读在屏障之前被移动。
的问题是:,确保在阅读之前提交轻松的原子写入的正确方法是什么?我现在的代码正确吗?(假设函数和类型按预期映射到std库构造。)
void* Allocator::Alloc(size_t bytes, size_t alignment)
{
void* p = AlignedAlloc(bytes, alignment);
AtomicFetchAdd(&allocatedBytes, AlignedMsize(p), MemoryOrder::Relaxed);
return p;
}
void Allocator::Free(void* p)
{
AtomicFetchSub(&allocatedBytes, AlignedMsize(p), MemoryOrder::Relaxed);
AlignedFree(p);
}
size_t Allocator::GetAllocatedBytes()
{
AtomicThreadFence(MemoryOrder::AcqRel);
return AtomicLoad(&allocatedBytes, MemoryOrder::Relaxed);
}以及上下文的某些类型定义。
enum struct MemoryOrder
{
Relaxed = 0,
Consume = 1,
Acquire = 2,
Release = 3,
AcqRel = 4,
SeqCst = 5,
};
struct Allocator
{
void* Alloc (size_t bytes, size_t alignment);
void Free (void* p);
size_t GetAllocatedBytes();
Atomic<size_t> allocatedBytes = { 0 };
};我不想简单地默认为顺序一致性,因为我试图更好地理解内存排序。
真正让我感到震惊的是,在[atomics.fences]下的标准中,所有的要点都谈到了获取围栏/原子操作与发布围栏/原子op同步。对于我来说,获取栅栏/原子操作是否会与另一个线程上的轻松原子操作同步是完全不透明的。如果AcqRel fence函数真的映射到maps指令,那么上面的代码就会很好了。然而,我很难说服自己,标准保证了这一点。即,
4一个原子操作A是对一个原子对象M的释放操作,如果在M上存在一些原子操作X,使得X在B之前被排序,并且读取A所写的值或由A领导的释放序列中任何一方ff等写的值,则它与获取栅栏B同步。
这似乎清楚地表明,栅栏不会与轻松的原子写入同步。另一方面,一个完整的围栏既是一个释放,也是一个获取围栏,所以它应该与自己同步,对吗?
2释放栅栏A与获取栅栏B同步,如果存在原子操作X和Y,这两个操作都操作在某个原子对象M上,使得A在X、X修改M、Y之前被测序,Y读取由X写入的值或假设释放序列X中任何一方eff等写入的值。
所描述的场景是
然而,在我的例子中,我没有原子写+原子读作为线程之间的信号,释放栅栏发生在线程B上的获取栅栏上。
显然,如果围栏在未排序的写开始前执行,这是一场比赛,所有的赌注都取消了。但在我看来,如果围栏在未排序的写入开始后执行,但在提交之前,它将被迫在未排序的读取之前完成。这正是我想要的,但我无法判断这是否得到了标准的保证。
发布于 2018-09-14 04:00:59
假设您生成名为Allocator::Alloc()的线程A,然后立即生成名为Allocator::GetAllocatedBytes()的线程B。这两个Allocator调用现在正在并发运行。你不知道哪一个会首先发生,因为它们之间没有顺序。您唯一的保证是线程B在线程A修改它之前看到allocatedBytes的值,或者在线程A修改它之后看到allocatedBytes的值。在GetAllocatedBytes()返回之前,您将不知道线程B所看到的值。(至少线程B不会看到allocatedBytes的完全垃圾值,因为由于您使用了轻松的atomics,所以没有数据竞争。)
您似乎关心线程A在AtomicFetchAdd()中得到的情况,但是由于某些原因,当线程B调用AtomicLoad()时,这种更改是不可见的。但那又怎样?这与GetAllocatedBytes()完全在AtomicFetchAdd()之前运行的结果没有什么不同。这是一个完全合理的结果。记住,线程B要么看到修改后的值,要么看不到。
即使将所有原子操作/栅栏更改为MemoryOrder::SeqCst,也不会有任何区别。在我描述的场景中,线程B仍然可以看到allocatedBytes的修改值或未修改的值,因为两个Allocator调用同时运行。
只要您坚持在其他线程仍在调用GetAllocatedBytes()和Free()时调用Alloc(),那么这就是您所能期望的最大的调用。如果您想获得一个更“准确”的值,只需在运行Alloc()/Free()时不允许对GetAllocatedBytes() /Free()进行任何并发调用!例如,如果程序正在关闭,只需在调用GetAllocatedBytes()之前加入所有其他线程即可。这将给出关机时分配的字节的准确数量。C++标准甚至保证了这一点,因为线程的完成与连接()的调用同步。。
发布于 2018-09-13 19:02:24
这将不能正常工作,acq_rel内存顺序是专门为CAS和FAA内存操作设计的,它们“同时”读取和写入原子数据。在这种情况下,您希望在加载前强制执行内存同步。为此,您需要将fetchAndAdd和fetchAndSub的内存顺序更改为acq_rel,将加载更改为acquire。这看起来可能很大,但在x86上它的成本非常低(一些编译器优化),因为它不会在代码中生成任何新的指令。关于获取发布同步的工作方式,我推荐本文:http://preshing.com/20120913/acquire-and-release-semantics/。
我删除了关于顺序排序的信息,因为它应该用于所有操作的正常工作,这将是一个过度。
根据我对C++原子的理解,当与使用内存栅栏的其他原子操作结合使用时,轻松的内存顺序是有意义的。例如,在某些情况下,原子a可以以轻松的方式存储,因为原子b是用释放内存顺序编写的,等等。
发布于 2018-09-14 05:57:46
如果您的问题是,在读取相同的原子对象之前,确保轻松的原子写入被提交的正确方法是什么?没有,这是由语言intro.multithread确保的。
对一个特定原子物体M的所有修改都是以某种特定的全序进行的,称为M的修改顺序。
所有线程都看到相同的修改顺序。例如,假设2分配发生在两个不同的线程中,然后在第三个线程中读取计数器。
在第一个线程中,原子增加了1个字节,轻松读取/修改(AtomicFetchAdd)表达式返回0:计数器完成了这个转换: 0->1。
在第二个线程中,原子的增量为2个字节,轻松读取/修改表达式返回1:计数器进行此转换: 1->3。读取/修改表达式无法返回0。此线程无法看到转换0->2,因为其他线程已执行转换0->1。
然后,在第三个线程中,执行轻松的负载。唯一可能加载的值是0、1或3。加载2是不可能的。原子的修改顺序是0 -> 1 -> 3。观察者线程也会看到这个修改顺序。
https://stackoverflow.com/questions/52318299
复制相似问题