将 std::condition_variable 与 atomic<bool> 一起使用

新手上路,请多包涵

有几个关于 SO 处理原子的问题,以及其他处理 std::condition_variable 的问题。但是我的问题是我在下面的使用是否正确?

三个线程,一个 ctrl 线程在取消暂停其他两个线程之前进行准备工作。 ctrl 线程还能够在工作线程(发送方/接收方)处于紧密的发送/接收循环中时暂停它们。使用原子的想法是在未设置暂停布尔值的情况下使紧密循环更快。

 class SomeClass
{

public:
    //...
    // Disregard that data is public...

    std::condition_variable cv; // UDP threads will wait on this cv until allowed
                                // to run by ctrl thread.
    std::mutex cv_m;
    std::atomic<bool> pause_test_threads;
};

void do_pause_test_threads(SomeClass *someclass)
{
    if (!someclass->pause_test_threads)
    {
        // Even though we use an atomic, mutex must be held during
        // modification. See documentation of condition variable
        // notify_all/wait. Mutex does not need to be held for the actual
        // notify call.
        std::lock_guard<std::mutex> lk(someclass->cv_m);
        someclass->pause_test_threads = true;
    }
}

void unpause_test_threads(SomeClass *someclass)
{
    if (someclass->pause_test_threads)
    {
        {
            // Even though we use an atomic, mutex must be held during
            // modification. See documentation of condition variable
            // notify_all/wait. Mutex does not need to be held for the actual
            // notify call.
            std::lock_guard<std::mutex> lk(someclass->cv_m);
            someclass->pause_test_threads = false;
        }
        someclass->cv.notify_all(); // Allow send/receive threads to run.
    }
}

void wait_to_start(SomeClass *someclass)
{
    std::unique_lock<std::mutex> lk(someclass->cv_m); // RAII, no need for unlock.
    auto not_paused = [someclass](){return someclass->pause_test_threads == false;};
    someclass->cv.wait(lk, not_paused);
}

void ctrl_thread(SomeClass *someclass)
{
    // Do startup work
    // ...
    unpause_test_threads(someclass);

    for (;;)
    {
        // ... check for end-program etc, if so, break;
        if (lost ctrl connection to other endpoint)
        {
            pause_test_threads();
        }
        else
        {
            unpause_test_threads();
        }
        sleep(SLEEP_INTERVAL);

    }

    unpause_test_threads(someclass);
}

void sender_thread(SomeClass *someclass)
{
    wait_to_start(someclass);
    ...
    for (;;)
    {
        // ... check for end-program etc, if so, break;
        if (someclass->pause_test_threads) wait_to_start(someclass);
        ...
    }
}

void receiver_thread(SomeClass *someclass)
{
    wait_to_start(someclass);
    ...
    for (;;)
    {
        // ... check for end-program etc, if so, break;
        if (someclass->pause_test_threads) wait_to_start(someclass);
        ...
    }

原文由 Erik Alapää 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 799
2 个回答

我查看了您操作条件变量和原子的代码,它似乎是正确的并且不会引起问题。

为什么即使它是原子的,你也应该保护对共享变量的写入:

如果在谓词中检查共享变量和等待条件之间发生写入共享变量,则可能会出现问题。考虑以下:

  1. 等待线程 虚假唤醒,获取互斥体,检查谓词并将其评估为 false ,因此它必须再次等待 cv。

  2. 控制线程 将共享变量设置为 true

  3. 控制线程 发送通知,任何人都没有收到,因为没有线程在等待条件变量。

  4. 等待线程 等待条件变量。由于通知已经发送,它会等到下一次虚假唤醒,或者控制线程下一次发送通知时。可能无限期地等待。

从没有锁定的共享原子变量中读取通常是安全的,除非它引入了 TOCTOU 问题

在您的情况下,您正在读取共享变量以避免不必要的锁定,然后在锁定后再次检查它(在有条件的 wait 调用中)。这是一个有效的优化,称为双重检查锁定,我在这里看不到任何潜在的问题。

您可能想检查 atomic<bool> 是否无锁。否则,如果没有它,您将拥有更多的锁。

原文由 Revolver_Ocelot 发布,翻译遵循 CC BY-SA 4.0 许可协议

通常,您希望独立于变量与条件变量一起工作的方式来对待变量是原子的这一事实。

如果与条件变量交互的所有代码都遵循在查询/修改之前锁定互斥锁的通常模式,并且与条件变量交互的代码不依赖于不与条件变量交互的代码,那么它将继续正确即使它包装了一个原子互斥体。

通过快速阅读您的伪代码,这似乎是正确的。但是,对于多线程代码,伪代码通常不能很好地替代真实代码。

当原子读取表明您可能想要也可能不是优化时,仅等待条件变量(并锁定互斥锁)的“优化”。您需要分析吞吐量。

原文由 Yakk - Adam Nevraumont 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题