共享内存的原子访问

15

我有一个被多个进程共享的内存,这些进程以某种特定方式解释这块内存。

DataBlock {
int counter;
double value1;
double ...    }

我想要的是计数器在原子级别上进行更新/递增,同时在该地址上进行内存释放操作。 例如,如果我没有使用共享内存,那么就会像这样:

std::atomic<int> counter;
atomic_store(counter, newvalue, std::memory_order_release); // perform release     operation on the affected memory location making the write visible to other threads

我如何为一个随机的内存位置(解释为上面的DataBlock计数器)实现这个?我可以保证地址按照架构要求对齐(x86 Linux)。

  1. 如何使更新操作具有原子性 - 怎么做?(即atomicupdate(addr, newvalue)
  2. 多核内存同步 - (即memorysync(addr))- 我唯一能想到的方法是使用std::atomic_thread_fence(std::memory_order_release),但这将“建立所有原子和松散原子存储的内存同步顺序” - 这对我来说过于复杂了 - 我只想同步计数器位置。感谢任何想法。

3
我只是猜测,但我认为C++编程模型没有"进程"的概念,内存模型也没有"共享内存"的概念,因此我怀疑标准本身不会做出任何保证。共享内存非常依赖于平台,所以请参考您所用平台的文档。 - Kerrek SB
1
你能在你的DataBlock中放置一个atomic<int>吗?只要atomic<int>是无锁的(标准显式地提到了进程间共享内存作为这些用例),那应该可以工作。但是你不能仅仅为一个随机地址获取一个原子变量(请参见https://dev59.com/vGoy5IYBdhLWcg3wJKoq#8749474)。@Kerrek SB:实际上,这种情况在[atomics.lockfree]中的最终草案中有所提及。 - Grizzly
@Grizzly:你是指非规范注释29.4/3吗?非常有趣,我不知道这个。 - Kerrek SB
如果内存是共享的,为什么缓存一致性会有所不同?我需要一种方法来同步特定地址在各个核心上的内存。如果C++不支持它,有人知道我可以使用哪些汇编指令吗?我读到在x86上,更新本身就是原子的,所以我想这个问题已经解决了。 - excalibur
@excalibur:这是针对操作系统和架构的特定要求。 - ildjarn
2
std::atomic 只在同一进程内的线程之间起作用,而不在进程之间起作用。一个进程不关心另一个进程中 std::atomic 的使用。 - Valentin H
3个回答

16

我无法在这里做出权威的回答,但我可以提供相关信息,可能会有所帮助。

  1. 互斥锁可以在共享内存中创建和/或创建为跨进程。Pthread具有特殊的创建标志,我记不清它是否使用共享内存,或者您是否要共享句柄。Linux的"futex"可以直接使用共享内存(请注意用户地址可能不同,但底层真实地址应相同)

  2. 硬件原子操作作用于内存而非进程变量。也就是说,芯片不会在意哪些程序修改了变量,最底层的原子操作自然就是跨进程的。同样适用于屏障。

  3. C++11未能指定跨进程原子操作。然而,如果它们是无锁的(检查标志),很难想象编译器如何实现它们,以使跨进程不起作用。但你会对你的工具链和最终平台寄予很多信心。

  4. CPU依赖性保证也跟踪实际内存地址,因此只要您的程序在线程形式下正确,将其更改为多进程形式(与可见性相关)也应该是正确的。

  5. Kerrek是正确的,抽象机器实际上并没有提及多个进程。然而,它的同步细节是以一种方式编写的,使它们同样适用于进程间和多线程,这与#3有关:编译器很难出错。

简短的答案是,没有符合标准的方法来做到这一点。但是,借助标准定义多线程的方式,您可以对质量良好的编译器进行许多假设。

最大的问题是是否可以在共享内存中分配原子对象(放置new)并正常工作。显然,这仅适用于真正的硬件原子操作。不过,我的猜测是,使用高质量的编译器/库,C++原子操作应该可以在共享内存中正常工作。

祝您验证行为愉快。:)


ISO C++确实规定无锁原子应该是无地址的。这种情况适用于普通CPU,其中原子性基于物理地址而不是虚拟地址。 - Peter Cordes
如果std::atomic不是无锁的,那么它绝对不会起作用。当前实现使用锁的哈希表,因此不同的进程将使用不同的哈希表。std::atomic的锁在哪里?。所以如果你有C++17,可以使用static_assert(std::atomic<T>::is_always_lock_free, "atomic<T> not lock free, can't work in shared mem")来检查这个便宜的编译时检查。 - Peter Cordes

7

由于您使用的是Linux,您可以在counter地址上使用gcc原子内置函数__sync_fetch_and_add()...根据gcc原子内置函数文档,这也将实现完整的内存屏障,而不是释放操作,但由于您实际上想要进行读取修改写入操作而不仅仅是加载(即增加计数器不仅仅是加载,而是需要读取、修改和最后写回值),全内存屏障将是一个更好的选择,以强制执行此操作的正确内存排序。


__sync_fetch_and_add() 可以使用,但我认为 __sync_sub_and_fetch() 更合适。你可以将引用计数减一,如果它降至零则释放。如果两个线程同时从2开始减,则保证只有一个会返回0(并释放)。 - ugoren
这绝对是一个好建议...一个原子内置函数可以用于在复制过程中进行递增,另一个则用于在引用不再使用时减少值。 - Jason

3
我正在查看标准草案N4820 [atomics.lockfree],其中提到:
4 [注:无锁操作也应该是无地址的。也就是说,通过两个不同地址对同一内存位置进行原子操作将进行原子通信。实现不应依赖于任何进程特定状态。这个限制允许通过映射到多个进程中的内存以及共享在两个进程之间的内存来进行内存通信。—注意结束]
因此,如果您想要无地址,则前提是无锁,可以通过std::atomic来检查。
但是,我不确定atomic对象应该如何创建。将对象放入共享内存中是否足够好?虽然我在github上看到了这样的代码用法,但是我没有找到任何规范关于此用法的说明。
(编辑说明:您可以将共享内存的指针转换为atomic对象指针,例如:
auto p = static_cast<atomic<int>*>(ptr); 与访问任何其他类型的共享内存相同。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接