两个不同的进程在同一地址上有 2 std::atomic 个变量?

Two Different Processes With 2 std::atomic Variables on Same Address?

我阅读了 C++ 标准 (n4713) 的 § 32.6.1 3:

Operations that are lock-free should also be address-free. That is, atomic operations on the same memory location via two different addresses will communicate atomically. The implementation should not depend on any per-process state. This restriction enables communication by memory that is mapped into a process more than once and by memory that is shared between two processes.

所以听起来好像可以在同一内存位置上执行无锁原子操作。我想知道怎么做。

假设我在 Linux 上有一个命名的共享内存段(通过 shm_open() 和 mmap())。例如,如何对共享内存段的前 4 个字节执行无锁操作?

起初,我以为我可以 reinterpret_cast 指向 std::atomic<int32_t>* 的指针。但后来我读了 this。它首先指出 std::atomic 可能不具有相同大小的 T 或对齐方式:

When we designed the C++11 atomics, I was under the misimpression that it would be possible to semi-portably apply atomic operations to data not declared to be atomic, using code such as

int x; reinterpret_cast<atomic<int>&>(x).fetch_add(1);

This would clearly fail if the representations of atomic and int differ, or if their alignments differ. But I know that this is not an issue on platforms I care about. And, in practice, I can easily test for a problem by checking at compile time that sizes and alignments match.

不过,在这种情况下我没问题,因为我在同一台机器上使用共享内存,并且在两个不同的进程中投射指针将 "acquire" 相同的位置。但是,文章指出编译器可能不会将转换后的指针视为指向原子类型的指针:

However this is not guaranteed to be reliable, even on platforms on which one might expect it to work, since it may confuse type-based alias analysis in the compiler. A compiler may assume that an int is not also accessed as an atomic<int>. (See 3.10, [Basic.lval], last paragraph.)

欢迎任何意见!

C++ 标准本身不关心多进程,所以不可能有任何正式的答案。这个答案将假设程序在同步方面与进程的行为或多或少与线程相同。

第一个解决方案需要 C++20 atomic_ref

void* shared_mem = /* something */

auto p1 = new (shared_mem) int;  // For creating the shared object
auto p2 = (int*)shared_mem;      // For getting the shared object

std::atomic_ref<int> i{p2};      // Use i as if atomic<int>

这可以防止共享内存中存在不透明的原子类型,从而使您可以精确控制其中的内容。

C++20 之前的解决方案是

auto p1 = new (shared_mem) atomic<int>;  // For creating the shared object
auto p2 = (atomic<int>*)shared_mem;      // For getting the shared object

auto& i = *p2;

或使用 C11 atomic_load and atomic_store

volatile int* i = (volatile int*)shared_mem;
atomic_store(i, 42);
int i2 = atomic_load(i);

是的,C++ 标准对这一切有点含糊其词。

如果您在 Windows(您可能不是),那么您可以使用 InterlockedExchange() 等,它们提供所有必需的语义并且不关心引用对象在哪里(很长 *).

在其他平台上,gcc 有一些 atomic builtins 可能对此有所帮助。它们可能会使您摆脱标准制定者的专制统治。问题是,很难测试生成的代码是否防弹。

在所有主流平台上,std::atomic<T> 确实与 T 具有相同的大小,但如果 T 的 alignof < sizeof.

可能会有更高的对齐要求

您可以通过以下方式检查这些假设:

  static_assert(sizeof(T) == sizeof(std::atomic<T>), 
            "atomic<T> isn't the same size as T");

  static_assert(std::atomic<T>::is_always_lock_free,  // C++17
            "atomic<T> isn't lock-free, unusable on shared mem");

  auto atomic_ptr = static_cast<atomic<int>*>(some_ptr);
           // beware strict-aliasing violations
           // don't also access the same memory via int*
           // unless you're aware of possible issues
      // also make sure that the ptr is aligned to alignof(atomic<T>)
      // otherwise you might get tearing (non-atomicity)

在这些不正确的奇异 C++ 实现中,想要在共享内存上使用您的代码的人将需要做其他事情。

或者如果所有从所有进程访问共享内存一致使用atomic<T>那么没有问题,你只需要无锁来保证无地址。 (您确实需要检查一下:std::atomic 使用锁的散列 table 用于非无锁。这是地址相关的,单独的进程将有单独的散列 table锁。)