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

Dav*_*aim 7 c++ linux futex stdatomic c++20

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

不幸的是,虽然wait已经标准化,wait_forwait_until不是。这意味着我们不能在超时的原子变量上睡觉。

无论如何,使用 Windows上的WaitOnAddress和Linux 上的futex系统调用在幕后实现原子变量睡眠。

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

在 Linux 上,不清楚是否可以std::atomicfutex. futex得到无论是uint32_t*int32_t*(取决于你读这手册),铸造std::atomic<u/int>u/int*为UB。另一方面,手册说

uaddr 参数指向 futex 字。 在所有平台上,futex 都是四字节整数,必须在四字节边界上对齐。在 futex_op 参数中指定要对 futex 执行的操作;val 是一个值,其含义和目的取决于 futex_op。

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

此外,我已经看到很多地方实现了这种结合原子和 futexes 的技巧,包括boostTBB

那么以非 UB 方式在具有超时的原子变量上睡眠的最佳方法是什么?我们是否必须使用操作系统原语来实现我们自己的原子类才能正确实现它?

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

Hum*_*ler 6

您不一定必须实现完整的自定义atomicAPI,实际上,只需从 中提取指向底层数据的指针atomic<T>并将其传递给系统就应该是安全的。

由于std::atomic不提供native_handle与其他同步原语相同的功能,因此您将不得不进行一些特定于实现的 hack 来尝试使其与本机 API 进行交互。

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

...并且投射std::atomic<u/int>u/int*UB

实际情况并非如此。

std::atomic标准保证为Standard-Layout Typereinterpret_cast标准布局类型的一个有用但通常深奥的属性是,对于第一个子对象(例如 的第一个成员)T的值或引用是安全的std::atomic

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

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

这种方法还应该在 Windows 上保持不变,atomic在将其传递给 API 之前将其强制转换为基础类型void*

注意:传递一个T*指向 a 的指针void*,该指针被重新解释为 a U*(例如当它需要 a 时的atomic<T>*to )是未定义的行为——即使有标准布局保证(据我所知)。它仍然可能有效,因为编译器无法查看系统 API——但这并不能使代码格式良好。void*T*

注 2:我不能谈论WaitOnAddressAPI,因为我实际上没有使用过它——但是任何依赖于正确对齐的整数值(void*或其他)的地址的原子 API 都应该通过提取指向潜在价值。


[1]由于它被标记为,您可以通过以下方式C++20验证:std::is_layout_compatiblestatic_assert

static_assert(std::is_layout_compatible_v<int,std::atomic<int>>);
Run Code Online (Sandbox Code Playgroud)

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

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