使用std::atomic和futex系统调用

12
在C++20中,我们获得了在原子变量上休眠并等待其值改变的能力。我们通过使用std::atomic::wait方法实现这一点。不幸的是,虽然wait已经标准化,但wait_forwait_until没有。这意味着我们不能在具有超时的原子变量上休眠。无论如何,在原子变量上休眠都是通过Windows上的WaitOnAddress和Linux上的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。另一方面,手册中说:
引用: uaddr参数指向futex字。在所有平台上,futexes都是四字节整数,必须对齐到四字节边界。在futex_op参数中指定要执行的操作; val是一个值,其含义和目的取决于futex_op。
提示alignas(4)std::atomic<int>应该可以工作,并且不管是哪种整数类型,只要类型大小为4字节且对齐为4就可以。 此外,我已经看到许多地方实现了将原子和futexes结合起来的技巧,包括boostTBB
那么,在非UB的情况下,使用原子变量进行睡眠并设置超时的最佳方法是什么? 我们是否需要使用操作系统原语实现自己的原子类来正确实现它?
(存在混合使用原子和条件变量的解决方案,但不是最佳选择)

“WaitOnAddress”是条件变量的有限实现,原子性并不重要。因此,为什么不尝试使用标准库中的经典条件变量,而不是使用原子操作呢? - facetus
@facetus 主要是吞吐量。 - David Haim
“WaitOnAddress”与原子操作无关,我非常确定与“std::condition_variable”相比不会带来任何好处。“WaitOnAddress”从语义上讲是一个条件变量,它只是在幕后隐藏了显式互斥锁。除此之外,它的功能完全相同。 - facetus
2个回答

6

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

由于std::atomic没有像其他同步原语提供的native_handle等价物,因此您将被困在执行特定于实现的hack以尝试使其与本机API接口。

就整数值而言,可以合理地假设这些类型的第一个成员与T类型相同,至少在实现中是如此。 这是一种保证,可以使提取出该值成为可能。

... 将std::atomic<u/int>强制转换为u/int*是未定义行为

事实并非如此。

std::atomic被标准保证为标准布局类型。标准布局类型的一个有用但常常晦涩的特性是,可以安全地将T转换为第一个子对象的值或引用(例如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*指针传递给一个将其重新解释为U*void*(例如,将atomic<T>*传递给期望T*void*)是未定义行为 — 即使具有标准布局保证(据我所知)。它仍然可能会工作,因为编译器无法看到系统API — 但这并不意味着代码良好形式。
注意2:我不能对WaitOnAddress API发表评论,因为我实际上没有使用过这个API — 但任何依赖于正确对齐整数值的地址(void*或其他)的原子API都应该通过提取出底层值的指针来正常工作。

[1] 因为这被标记为 C++20,所以您可以使用 std::is_layout_compatiblestatic_assert 来验证此内容:

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

感谢 @apmccartney 在评论中提供的建议。

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


有关UB的有趣观点。如果没有同步就将 “int *” 解引用为 “atomic <int>” 对象,以确保在此时没有其他线程正在读取/写入它,则会出现UB问题。但是,将其传递给 futex 基本上就像在该 int 对象上使用 atomic_ref<int> 操作-您正在调用旨在安全地处理同时进行读取+写入的其他线程的机器代码。只要 atomic<T> 是无锁的; 如果您还执行诸如 alignas(4) std::atomic<int> 的裤带和吊带之类的操作,也许对 is_always_lock_free 进行静态断言将是个好主意。 - Peter Cordes
1
这正是我所说的“无需同步……”:你可能会创建数据竞争 UB,而不是严格别名 UB。 - Peter Cordes
1
回复:脚注[1]这可以通过编程进行验证。请参见std::is_layout_compatible。 https://en.cppreference.com/w/cpp/types/is_layout_compatible - apmccartney
@apmccartney 很好的建议!我还没有完全探索C++20中添加的所有新特性,所以感谢你引起了我的注意。 - Human-Compiler
static_assert(sizeof(int) == sizeof(std::atomic<int>) && std::atomic<int>::is_always_lock_free) 是在 C++17 中进行合理的尝试检查,但在没有 C++20 的情况下。如果它们具有相同的大小并且 atomic_int 是 lock_free 的,则排除了大多数可能的不兼容性。 - Peter Cordes
显示剩余5条评论

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接