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

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

本文关键字:std atomic 地址 变量 两个 进程      更新时间:2023-10-16

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

锁的操作也应无地址。那是 通过两个不同的对同一内存位置进行原子操作 地址将以原子方式进行通信。实现不应 取决于任何每个进程的状态。此限制启用 通过多次映射到进程中的内存进行通信 以及两个进程之间共享的内存。

因此,听起来可以在同一内存位置执行无锁原子操作。 我想知道如何做到这一点。

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

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

当我们设计C++11原子学时,我有一种错误的印象: 可以半可移植地将原子操作应用于数据 未声明为原子,使用

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

如果原子和 int 的表示 不同,或者它们的对齐方式不同。但我知道这不是一个 我关心的平台上的问题。而且,在实践中,我可以轻松测试 通过在编译时检查大小和对齐方式来解决问题 火柴。

托,在这种情况下对我来说很好,因为我在同一台机器上使用共享内存,并且在两个不同的进程中转换指针将"获取"相同的位置。 但是,文章指出编译器可能不会将强制转换的指针视为指向原子类型的指针:

但是,这并不能保证是可靠的,即使在以下平台上也是如此 哪一个可能希望它工作,因为它可能会混淆基于类型 编译器中的别名分析。编译器可能假定 int 是 不能作为atomic<int>访问。(参见 3.10, [Basic.lval], 最后 段。

欢迎任何意见!

C++标准不涉及多个进程,并且在多线程环境之外没有给出任何保证。 但是,该标准确实建议无锁原子的实现可以跨进程使用,这在大多数实际实现中都是如此。 这个答案将假设原子在进程中的行为与线程的行为大致相同。

第一种解决方案需要 C++20atomic_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>

您需要确保共享int具有std::atomic_ref<int>::required_alignment对齐;通常与sizeof(int)相同。 通常,您会在结构成员或变量上使用alignas(),但在共享内存中,布局由您决定(相对于已知的页面边界)。

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

<小时 />

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;

或使用 C11atomic_loadatomic_store

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

对齐要求在这里是相同的,alignof(std::atomic<int>)_Alignof(atomic_int)

是的,C++标准对这一切有点胡言乱语。

如果你在Windows上(你可能不是),那么你可以使用InterlockedExchange()等,它们提供了所有必需的语义,并且不在乎引用的对象在哪里(它是一个长*)。

在其他平台上,gcc 有一些原子内置,可能有助于解决这个问题。 他们可能会把你从标准编写者的暴政中解放出来。 问题是,很难测试生成的代码是否防弹。

在所有主流平台上,std::atomic<T>确实具有与T相同的大小,尽管如果T具有对齐方式<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使用锁的哈希表来实现非无锁。 这是与地址相关的,单独的进程将具有单独的锁哈希表。