在我的代码中,我想使用std::atomic_flag
来同步两个线程。具体来说,我想使用C++20中引入的新的wait
和notify_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"(。