调用notify_all后,是否可以安全地销毁std::atomic_flag



在我的代码中,我想使用std::atomic_flag来同步两个线程。具体来说,我想使用C++20中引入的新的waitnotify_all特性。

简而言之:一个线程正在等待标志就绪,而另一个线程将设置标志并发出通知。然而,问题是atomic_flag存在于堆栈中,并将在通知后被销毁,而第一个线程可能仍在对wait的调用中。

基本上,我有相当于以下片段的东西:

#include <atomic>
#include <thread>
int main(int, char**)
{
auto t = std::thread{};
{
auto f = std::atomic_flag{};
t = std::thread{[&f] { f.wait(false); }};
// Ensures that 't' is waiting on 'f' (not 100% guarantee, but you get the point)
std::this_thread::sleep_for(std::chrono::milliseconds{50});
f.test_and_set();
f.notify_all();
} // <--- 'f' is destroyed here but 't' may still be in the wait call
t.join();
return 0;
}

在过去,我曾在这种情况下使用boost::latch,从经验中我知道这种模式几乎总是会崩溃或断言。但是,用std::atomic_flag替换boost::latch并没有导致任何崩溃、断言或死锁。

我的问题:在调用notify_all之后销毁std::atomic_flag是否安全(即唤醒线程可能仍在wait方法中(?

,它不安全。

来自标准

在标准([atomics.flag](中,atomic_flag_wait的作用描述如下:

效果:按顺序重复执行以下步骤:

  • 评估flag->test(order) != old
  • 如果该评估的结果是true,则返回
  • 阻止,直到它被原子通知操作取消阻止或被错误地取消阻止

这意味着,在取消阻止之后,访问std::atomic_flag以读取新值。因此,这是一场从另一个线程破坏原子标志的竞赛。

在实践中

可能,代码片段工作得很好,因为std::atomic_flag的析构函数很琐碎。因此,堆栈上的内存保持不变,等待线程仍然可以继续使用这些字节,就像它们是原子标志一样。

通过对代码进行一点修改,将std::atomic_flag所在的内存显式清零,代码片段现在死锁了(至少在我的系统上是这样(。

#include <atomic>
#include <cstddef>
#include <cstring>
#include <thread>
int main(int, char**)
{
auto t = std::thread{};
// Some memory to construct the std::atomic_flag in
std::byte memory[sizeof(std::atomic_flag)];
{
auto f = new (reinterpret_cast<std::atomic_flag *>(&memory)) std::atomic_flag{};
t = std::thread{[&f] { f->wait(false); }};
std::this_thread::sleep_for(std::chrono::milliseconds{50});
f->test_and_set();
f->notify_all();

f->~atomic_flag(); // Trivial, but it doesn't hurt
// Set the memory where the std::atomic_flag lives to all zeroes
std::memset(&memory, 0, sizeof(std::atomic_flag));
}
t.join();
return 0;
}

如果在内存设置为全零后,等待线程碰巧读取了原子标志的值,这将使线程死锁(可能是因为它现在将这些零解释为原子标志值的"false"(。

相关内容

  • 没有找到相关文章

最新更新