在 futex 系统调用中使用 std::atomic

Using std::atomic with futex system call

在 C++20 中,我们可以休眠原子变量,等待它们的值发生变化。 我们通过使用 std::atomic::wait 方法来做到这一点。

不幸的是,虽然 wait 已经标准化,但 wait_forwait_until 还没有。这意味着我们不能在超时的情况下休眠原子变量。

休眠原子变量无论如何都是在幕后通过 Linux 上的 WaitOnAddress on Windows and the futex 系统调用实现的。

解决上述问题(无法在超时的情况下休眠原子变量),我可以将 std::atomic 的内存地址传递给 Windows 上的 WaitOnAddress 和它会(有点)在没有 UB 的情况下工作,因为函数将 void* 作为参数,并且将 std::atomic<type> 转换为 void*

是有效的

在 Linux 上,不清楚是否可以将 std::atomicfutex 混合使用。 futex 得到 uint32_t*int32_t*(取决于您阅读的手册),将 std::atomic<u/int> 转换为 u/int* 是 UB。另一方面,手册说

The uaddr argument points to the futex word. On all platforms, futexes are four-byte integers that must be aligned on a four- byte boundary. The operation to perform on the futex is specified in the futex_op argument; val is a value whose meaning and purpose depends on futex_op.

提示 alignas(4) std::atomic<int> 应该有效,只要类型具有 4 个字节的大小和 4 的对齐方式,它是哪种整数类型并不重要。

此外,我看到很多地方都实现了这种结合原子和 futexes 的技巧,包括 boost and TBB

那么以非 UB 方式在超时的原子变量上休眠的最佳方法是什么? 我们是否必须使用 OS 原语实现我们自己的原子 class 才能正确实现它?

(存在混合原子和条件变量等解决方案,但不是最优的)

您不一定要实现完整的自定义 atomic API,从 atomic<T> 和传递给系统。

由于 std::atomic 不像其他同步原语提供的那样提供 native_handle 的等价物,你将不得不做一些 implementation-specific hack 来尝试让它接口与原生 API.

在大多数情况下,假设实现中这些类型的第一个成员与 T 类型相同是相当安全的——至少对于整数值 [1] 。这是一种保证,可以提取此值。

... and casting std::atomic<u/int> to u/int* is UB

事实并非如此。

std::atomic is guaranteed by the standard to be Standard-Layout Type. One helpful but often esoteric properties of standard layout types is that it is safe to reinterpret_cast a T to a value or reference of the first sub-object(例如 std::atomic 的第一个成员)。

只要我们能保证 std::atomic<u/int> 只包含 u/int 作为一个成员(或者至少,作为它的第一个成员),那么以这种方式提取类型是完全安全的:

auto* r = reinterpret_cast<std::uint32_t*>(&atomic);
// Pass to futex API...

这种方法还应该坚持 windows 将 atomic 转换为基础类型,然后再将其传递给 void* API。

注意:T* 指针传递给 void*,它被重新解释为 U*(例如 atomic<T>*void* 当它期望 T*) 是 未定义的行为 - 即使有 standard-layout 保证(据我所知)。它仍然 可能工作 因为编译器无法查看系统 APIs -- 但这不会使代码 well-formed.

注意 2: 我不能在 WaitOnAddress API 上发言,因为我没有实际使用过它——但是任何原子 API 取决于正确对齐的整数值(void* 或其他)的地址,应该通过提取指向基础值的指针来正常工作。


[1] 由于这被标记为 C++20,您可以使用 std::is_layout_compatiblestatic_assert:[=43= 来验证这一点]

static_assert(std::is_layout_compatible_v<int,std::atomic<int>>);

(感谢@apmccartney 在评论中提出此建议)。

我可以确认这将与 Microsoft's STL, libc++, and libstdc++ 的布局兼容;但是,如果您无权访问 is_layout_compatible 并且您使用的是不同的系统,则可能需要检查编译器的 headers 以确保此假设成立。