【问题标题】:Can std::atomic_flag be safely destroyed after calling notify_all?调用 notify_all 后可以安全销毁 std::atomic_flag 吗?
【发布时间】:2022-01-25 03:39:46
【问题描述】:

在我的代码中,我想使用std::atomic_flag 来同步两个线程。具体来说,我想使用 C++20 中引入的新 wait 和 notify_all 功能。

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

基本上,我有相当于以下sn-p的东西:

#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 方法中)?

【问题讨论】:

  • 我相信这是一场比赛。 wait 被定义为在唤醒后检查标志的值,将其与其参数进行比较(并且可能重新进入睡眠状态)。这将与析构函数竞争。
  • @IgorTandetnik 是的,确实如此。我已经设法相当快地打破了工作代码 sn-p 使其陷入僵局。在读取原子标志值的唤醒线程和破坏原子标志的销毁线程之间确实存在竞争。在 C++ 标准中,wait 的效果也描述了在唤醒之后,线程会再次读取 flag 的值。

标签: c++ multithreading atomic c++20 lifetime


【解决方案1】:

不,不安全。

来自标准

在标准([atomics.flag])中,atomic_flag_wait的效果描述如下:

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

  • 评估flag-&gt;test(order) != old。
  • 如果该评估的结果是true,则返回。
  • 在被原子通知操作解除阻塞或被虚假解除阻塞之前一直阻塞。

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

在实践中

可能代码 sn-p 工作正常,因为std::atomic_flag 的析构函数是微不足道的。所以内存在堆栈上保持不变,等待线程仍然可以继续使用这些字节,就好像它们是原子标志一样。

通过稍微修改代码以将std::atomic_flag 所在的内存显式归零,sn-p 现在死锁(至少在我的系统上)。

#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;
}

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-10-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-26
    • 2015-05-24
    • 2014-01-04
    相关资源
    最近更新 更多