【问题标题】:Using std::atomic with futex system call将 std::atomic 与 futex 系统调用一起使用
【发布时间】:2021-07-06 02:19:16
【问题描述】:

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

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

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

解决上述问题(无法在超时的原子变量上休眠),我可以在 Windows 上将std::atomic 的内存地址传递给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 上执行的操作是 在 futex_op 参数中指定; val 是一个值,其含义 目的取决于 futex_op。

提示alignas(4) std::atomic<int> 应该可以工作,不管它是哪个整数类型,只要该类型的大小为 4 字节且对齐为 4。

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

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

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

【问题讨论】:

  • WaitOnAddress 是条件变量的有限实现,原子性无关紧要。那么,与其使用原子,不如试试标准库中的经典条件变量?
  • @facetus 吞吐量,主要是。
  • WaitOnAddress 与原子无关,与std::condition_variable 相比,我敢肯定不会给您带来任何好处。 WaitOnAddress 在语义上是一个条件变量,它只是将显式互斥锁隐藏在幕后。除此之外,它的作用完全相同。

标签: c++ linux c++20 stdatomic futex


【解决方案1】:

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

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

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

...将std::atomic<u/int> 转换为u/int* 是UB

事实并非如此。

std::atomic 由标准保证为Standard-Layout Type。标准布局类型的一个有用但通常深奥的属性是reinterpret_castT 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&lt;T&gt;* 需要T* 时将void* 传递给T*)是未定义的行为——即使有标准布局保证(据我所知)。它仍然可能会工作,因为编译器无法查看系统 API —— 但这不会使代码格式正确。

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


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

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

(感谢@apmccartney 在 cmets 中的建议)。

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

【讨论】:

  • 关于 UB 的有趣点。 UB deref int*atomic&lt;int&gt; 对象没有同步以确保当时没有其他线程正在读/写它,但是将它传递给 futex 基本上就像在该 int 对象上使用 atomic_ref&lt;int&gt; 操作 - 您正在调用旨在在其他线程同时读取+写入的情况下安全的机器代码。只要atomic&lt;T&gt; 是无锁的;如果你也在做类似alignas(4) std::atomic&lt;int&gt; 这样的腰带和吊带,也许is_always_lock_free 上的static_assert 会是一个好主意。
  • 我曾考虑在is_always_lock_free 上建议static_assert——但最终决定不这样做。以这种方式扩展atomic 几乎总是需要与std::atomic 实现进行某种 级别的耦合——此时可能必须对实现有一些了解才能真正分享这一点在本机系统 API 之间。同样对于“UB to deref an int* ...”点——技术上它不会是 UB,因为指针是合法且有效的。如果在线程上下文中访问它,它就不会被 sequenced (这将使它成为 UB)
  • 这正是我所说的“没有同步...”的意思:您可以创建数据竞争 UB,而不是严格混叠 UB。
  • Re: 脚注 [1] 这可以通过编程方式进行验证。见 std::is_layout_compatible。 en.cppreference.com/w/cpp/types/is_layout_compatible
  • @apmccartney 很好的建议!我还没有完全探索 C++20 中添加的所有新特性,所以感谢你让我注意到这一点
猜你喜欢
  • 1970-01-01
  • 2014-02-28
  • 1970-01-01
  • 2012-03-22
  • 2012-10-28
  • 2015-06-21
  • 2014-08-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多