10

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

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

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

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

在 Linux 上,是否可以std::atomicfutex. futex获取 auint32_t*或 a int32_t*(取决于您阅读的手册),并且转换std::atomic<u/int>u/int*UB。另一方面,手册说

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

提示alignas(4) std::atomic<int>应该可行,只要类型的大小为 4 字节且对齐为 4,它是哪种整数类型都没有关系。

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

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

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

4

1 回答 1

5

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

由于std::atomic不提供native_handle与其他同步原语提供的等价物,因此您将不得不进行一些特定于实现的黑客攻击以尝试使其与本机 API 接口。

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

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

事实并非如此。

std::atomic标准保证为Standard-Layout Type。标准布局类型的一个有用但通常深奥的属性是对第一个子对象(例如 的第一个成员)的值或引用reinterpret_cast是安全的。Tstd::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*被重新解释为 a 的 a U*(例如当它期望 a 时的 a atomic<T>*)是未定义的行为——即使有标准布局保证(据我所知)。它仍然可能会工作,因为编译器无法查看系统 API——但这不会使代码格式正确。void*T*

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


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

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

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

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

于 2021-04-21T14:01:25.113 回答