与松弛原子同步

时间:2018-09-13 16:47:31

标签: c++ multithreading concurrency atomic

我有一个分配器,它执行宽松的原子以跟踪当前分配的字节数。它们只是加法和减法,所以除了确保修改是原子化的,我不需要线程之间的任何同步。

但是,我偶尔要检查分配的字节数(例如,在关闭程序时),并希望确保提交任何未决的写入。我假设在这种情况下我需要一个完整的内存屏障,以防止将先前的任何写入移动到屏障之后,并防止将下一个读取移动到屏障之前。

问题是:什么是确保阅读之前提交宽松的原子写的正确方法是什么?我当前的代码正确吗? (假设函数和类型按预期映射到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]下的标准中,所有要点都讨论了与发布篱笆/原子操作同步的获取篱笆/原子操作。对于我来说,获取栅栏/原子操作是否将与另一线程上的宽松原子操作同步是完全不透明的。如果AcqRel篱笆函数按字面意义映射到mfence指令,则似乎上面的代码会很好。但是,我很难说服自己标准可以保证这一点。即

  

4原子操作A,它是对原子的释放操作   如果存在对象M,则对象M与获取栅栏B同步   M上的原子操作X,使得X在B之前排序并读取   由A编写的值或由A的任意一方写的值   以A为首的释放顺序。

这似乎很清楚地表明,篱笆将不会与宽松的原子写入同步。另一方面,完整的篱笆既是发布篱笆又是获取篱笆,因此它应该与自身同步,对吧?

  

2如果存在释放栅栏A,则与获取栅栏B同步   存在原子操作X和Y,它们都对某个原子对象进行操作   M,这样A在X之前先被排序,X修改M,Y在先序列   在B之前,Y读取X编写的值或任何Any编写的值   假设释放序列X中的副作用将在   是一个释放操作。

描述的场景是

  • 无序写入
  • 释放栅栏
  • X原子写入
  • Y原子读取
  • B获取围栏
  • 未排序的读取(此处将显示未排序的写入)

但是,在我的情况下,我没有原子写+原子读作为线程与释放栅栏之间的信号,而线程B上的获取栅栏发生了。所以实际上是在发生

  • 无序写入
  • 释放栅栏
  • B获取围栏
  • 无序读取

很明显,如果围墙在无序写入开始之前执行,那是一场比赛,所有赌注都关闭了。但是在我看来,如果篱笆在无序写入开始后但在提交之前执行,它将被迫在无序读取之前完成。这正是我想要的,但是我无法收集是否由标准保证

2 个答案:

答案 0 :(得分:4)

比方说,您生成了线程A,它调用了Allocator::Alloc(),然后立即生成了线程B,它调用了Allocator::GetAllocatedBytes()。这两个Allocator调用现在正在同时运行。您不知道哪个最先发生,因为它们之间没有顺序。您唯一的保证是线程B在修改线程A之前会看到allocatedBytes的值,或者在线程A修改之后它会看到allocatedBytes的值。您需要知道GetAllocatedBytes()返回之后,线程B才能看到哪个值。 (至少线程B不会看到allocatedBytes的完全垃圾值,因为由于使用了宽松的原子,因此没有数据竞争。)

您似乎担心线程A到达AtomicFetchAdd()的情况,但是由于某种原因,当线程B调用AtomicLoad()时,更改不可见。但是那又怎样呢?这与GetAllocatedBytes()完全在AtomicFetchAdd()之前运行的结果没有什么不同。那是完全有效的结果。请记住,线程B看到的是修改后的值,或者没有。

即使将所有原子操作/围栏更改为MemoryOrder::SeqCst,也不会有任何不同。在我描述的场景中,线程B仍然可以看到allocatedBytes的修改后的值或未修改的值,因为两个Allocator调用是同时运行的。

只要您坚持在其他线程仍在调用GetAllocatedBytes()Alloc()的同时调用Free(),那实际上就是您可以期望的。如果要获得更“准确”的值,只需在运行Alloc()时不允许对Free() / GetAllocatedBytes()的任何并发调用!例如,如果程序正在关闭,则只需在调用GetAllocatedBytes()之前加入所有其他线程即可。这将为您提供关机时准确分配的字节数。 C ++标准甚至可以保证它,因为the completion of a thread synchronizes with the call to join()

答案 1 :(得分:0)

如果您的问题是确保在阅读同一原子对象之前提交轻松的原子写入的正确方法是什么?没什么,这是通过[intro.multithread]语言确保的:

  

对特定原子对象M的所有修改都以某种特定的总顺序发生,称为M的修改顺序

所有线程都具有相同的修改顺序。例如,假设2个分配发生在2个不同的线程中,然后在第三个线程中读取计数器。

在第一个线程中,原子增加1个字节,并且轻松的读/修改(AtomicFetchAdd)表达式返回0:计数器进行此转换:0-> 1。

在第二个线程中,原子增加2个字节,并且宽松的读取/修改表达式返回1:计数器进行此转换:1-> 3。读/修改表达式无法返回0。该线程看不到过渡0-> 2,因为另一个线程执行了过渡0-> 1。

然后在第三个线程中执行轻松的加载。可能加载的唯一可能的值为0,1或3。无法加载2。原子的修改顺序为0-> 1->3。观察者线程也将看到此修改顺序。 / p>