在 futex 系统调用中使用 std::atomic
Using std::atomic with futex system call
在 C++20 中,我们可以休眠原子变量,等待它们的值发生变化。
我们通过使用 std::atomic::wait
方法来做到这一点。
不幸的是,虽然 wait
已经标准化,但 wait_for
和 wait_until
还没有。这意味着我们不能在超时的情况下休眠原子变量。
休眠原子变量无论如何都是在幕后通过 Linux 上的 WaitOnAddress on Windows and the futex 系统调用实现的。
解决上述问题(无法在超时的情况下休眠原子变量),我可以将 std::atomic
的内存地址传递给 Windows 上的 WaitOnAddress
和它会(有点)在没有 UB 的情况下工作,因为函数将 void*
作为参数,并且将 std::atomic<type>
转换为 void*
是有效的
在 Linux 上,不清楚是否可以将 std::atomic
与 futex
混合使用。 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_compatible
和 static_assert
:[=43= 来验证这一点]
static_assert(std::is_layout_compatible_v<int,std::atomic<int>>);
(感谢@apmccartney 在评论中提出此建议)。
我可以确认这将与 Microsoft's STL, libc++, and libstdc++ 的布局兼容;但是,如果您无权访问 is_layout_compatible
并且您使用的是不同的系统,则可能需要检查编译器的 headers 以确保此假设成立。
在 C++20 中,我们可以休眠原子变量,等待它们的值发生变化。
我们通过使用 std::atomic::wait
方法来做到这一点。
不幸的是,虽然 wait
已经标准化,但 wait_for
和 wait_until
还没有。这意味着我们不能在超时的情况下休眠原子变量。
休眠原子变量无论如何都是在幕后通过 Linux 上的 WaitOnAddress on Windows and the futex 系统调用实现的。
解决上述问题(无法在超时的情况下休眠原子变量),我可以将 std::atomic
的内存地址传递给 Windows 上的 WaitOnAddress
和它会(有点)在没有 UB 的情况下工作,因为函数将 void*
作为参数,并且将 std::atomic<type>
转换为 void*
在 Linux 上,不清楚是否可以将 std::atomic
与 futex
混合使用。 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>
tou/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_compatible
和 static_assert
:[=43= 来验证这一点]
static_assert(std::is_layout_compatible_v<int,std::atomic<int>>);
(感谢@apmccartney 在评论中提出此建议)。
我可以确认这将与 Microsoft's STL, libc++, and libstdc++ 的布局兼容;但是,如果您无权访问 is_layout_compatible
并且您使用的是不同的系统,则可能需要检查编译器的 headers 以确保此假设成立。