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')。