std::atomic_flag调用notify_all后能否安全销毁?
Can std::atomic_flag be safely destroyed after calling notify_all?
在我的代码中,我想使用 C++20 中引入的 std::atomic_flag
to synchronize two threads. Specifically, I would like to use the new wait
and 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
,我从经验中知道这种模式几乎总是会崩溃或断言。但是,将 boost::latch
替换为 std::atomic_flag
并未导致任何崩溃、断言或死锁。
我的问题: 在调用 notify_all
之后销毁 std::atomic_flag
是否安全(即唤醒线程可能仍在 wait
方法)?
不,不安全
来自标准
在标准([atomics.flag]
)中,atomic_flag_wait
的作用描述如下:
Effects: Repeatedly performs the following steps, in order:
- Evaluates
flag->test(order) != old
.
- If the result of that evaluation is
true
, returns.
- Blocks until it is unblocked by an atomic notifying operation or is unblocked spuriously.
这意味着,解除阻塞后,访问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')。
在我的代码中,我想使用 C++20 中引入的 std::atomic_flag
to synchronize two threads. Specifically, I would like to use the new wait
and 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
,我从经验中知道这种模式几乎总是会崩溃或断言。但是,将 boost::latch
替换为 std::atomic_flag
并未导致任何崩溃、断言或死锁。
我的问题: 在调用 notify_all
之后销毁 std::atomic_flag
是否安全(即唤醒线程可能仍在 wait
方法)?
不,不安全
来自标准
在标准([atomics.flag]
)中,atomic_flag_wait
的作用描述如下:
Effects: Repeatedly performs the following steps, in order:
- Evaluates
flag->test(order) != old
.- If the result of that evaluation is
true
, returns.- Blocks until it is unblocked by an atomic notifying operation or is unblocked spuriously.
这意味着,解除阻塞后,访问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')。