条件变量真的需要另一个变量吗?

Does a condition variable really need another variable?

提问人:NPS 提问时间:3/30/2020 更新时间:2/11/2022 访问量:530

问:

注意:我将用 C++ 举例,但我相信我的问题与语言无关。如果我错了,请纠正我。

只是为了让你真正理解我 - 我在这里想学习的是该工具的作用,仅此而已。不是它通常的用途,不是约定所说的,只是钝器的作用。在本例中 - 条件变量的作用。

到目前为止,在我看来,这是一种简单的机制,允许线程等待(阻塞)直到其他线程发出信号(取消阻塞它们)。仅此而已,无需处理关键部分访问或数据访问(当然,它们可以用于此,但这只是程序员的选择问题)。此外,信令通常只在重要事件发生时(例如加载数据)进行,但理论上它可以随时调用。到目前为止正确吗?

现在,我看到的每个示例都使用条件变量对象(例如)以及一些附加变量来标记是否发生了某些事情(例如)。看看 https://thispointer.com//c11-multithreading-part-7-condition-variables-explained/ 的这个例子:std::condition_variablebool dataWasLoaded

#include <iostream>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std::placeholders;
class Application
{
    std::mutex m_mutex;
    std::condition_variable m_condVar;
    bool m_bDataLoaded;
public:
    Application()
    {
        m_bDataLoaded = false;
    }
    void loadData()
    {
        // Make This Thread sleep for 1 Second
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "Loading Data from XML" << std::endl;
        // Lock The Data structure
        std::lock_guard<std::mutex> guard(m_mutex);
        // Set the flag to true, means data is loaded
        m_bDataLoaded = true;
        // Notify the condition variable
        m_condVar.notify_one();
    }
    bool isDataLoaded()
    {
        return m_bDataLoaded;
    }
    void mainTask()
    {
        std::cout << "Do Some Handshaking" << std::endl;
        // Acquire the lock
        std::unique_lock<std::mutex> mlock(m_mutex);
        // Start waiting for the Condition Variable to get signaled
        // Wait() will internally release the lock and make the thread to block
        // As soon as condition variable get signaled, resume the thread and
        // again acquire the lock. Then check if condition is met or not
        // If condition is met then continue else again go in wait.
        m_condVar.wait(mlock, std::bind(&Application::isDataLoaded, this));
        std::cout << "Do Processing On loaded Data" << std::endl;
    }
};
int main()
{
    Application app;
    std::thread thread_1(&Application::mainTask, &app);
    std::thread thread_2(&Application::loadData, &app);
    thread_2.join();
    thread_1.join();
    return 0;
}

现在,除此之外,它还使用了一个附加变量。但在我看来,执行的线程已经收到通知,数据是通过 加载的。为什么还要检查相同的信息?比较(相同的代码没有):std::condition_variable m_condVarbool m_bDataLoadedmainTaskstd::condition_variable m_condVarbool m_bDataLoadedbool m_bDataLoaded

#include <iostream>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std::placeholders;
class Application
{
    std::mutex m_mutex;
    std::condition_variable m_condVar;
public:
    Application()
    {
    }
    void loadData()
    {
        // Make This Thread sleep for 1 Second
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "Loading Data from XML" << std::endl;
        // Lock The Data structure
        std::lock_guard<std::mutex> guard(m_mutex);
        // Notify the condition variable
        m_condVar.notify_one();
    }
    void mainTask()
    {
        std::cout << "Do Some Handshaking" << std::endl;
        // Acquire the lock
        std::unique_lock<std::mutex> mlock(m_mutex);
        // Start waiting for the Condition Variable to get signaled
        // Wait() will internally release the lock and make the thread to block
        // As soon as condition variable get signaled, resume the thread and
        // again acquire the lock. Then check if condition is met or not
        // If condition is met then continue else again go in wait.
        m_condVar.wait(mlock);
        std::cout << "Do Processing On loaded Data" << std::endl;
    }
};
int main()
{
    Application app;
    std::thread thread_1(&Application::mainTask, &app);
    std::thread thread_2(&Application::loadData, &app);
    thread_2.join();
    thread_1.join();
    return 0;
}
  1. 现在我知道了虚假唤醒,仅凭它们就需要使用一个额外的变量。我的问题是——他们只是这样做的原因吗?如果它们没有发生,是否可以只使用条件变量而不使用任何其他变量(顺便说一句,这不会使“条件变量”这个名字用词不当)?
  2. 另一件事是 - 使用其他变量难道不是条件变量也需要互斥锁的唯一原因吗?如果不是,其他原因是什么?
  3. 如果需要额外的变量(出于虚假唤醒或其他原因),为什么 API 不需要它们(在第二个代码中,我不必使用它们来编译代码)?(我不知道在其他语言中是否相同,所以这个问题可能是特定于 C++ 的。
多线程与 语言无关的 条件变量

评论


答:

3赞 Matt Timmermans 3/30/2020 #1

这不仅仅是关于虚假的唤醒。

当您致电时,您如何知道您正在等待的情况尚未发生?m_condvar.wait

也许“loadData”已经在另一个线程中被调用了。当它调用时,什么也没发生,因为没有线程在等待。notify_one()

现在,如果你打电话,你将永远等待,因为没有什么会向你发出信号。condvar.wait

原始版本没有这个问题,因为:

  1. 如果为 false,则知道数据未加载,设置为 true ,调用方将发出条件信号;m_bDataLoadedm_bDataLoaded
  2. 锁被持有,我们知道在释放之前不能在另一个线程中修改它;m_bDataLoaded
  3. condvar.wait在释放锁之前,会将当前线程放入等待队列中,因此我们知道在开始等待后会将其设置为 true,因此在我们开始等待也会被调用。m_bDataLoadednotify_one

要回答您的其他问题,请执行以下操作:

  • 是的,与其他变量的协调是条件变量与互斥锁绑定的原因。
  • 比如说,API 不需要布尔变量,因为这并不总是你正在等待的那种条件。

这种事情很常见,例如:

Task *getTask() {
    //anyone who uses m_taskQueue or m_shutDown must lock this mutex
    unique_lock<mutex> lock(m_mutex);

    while (m_taskQueue.isEmpty()) {
        if (m_shutdown) {
            return null;
        }
        // this is signalled after a task is enqueued
        // or m_shutdown is asserted
        m_condvar.wait(lock);
    }
    return taskQueue.pop_front();
}

这里我们需要同样的关键保证,即线程在释放锁之前开始等待,但我们等待的条件更复杂,涉及变量和单独的数据结构,并且有多种方法可以退出等待。

评论

0赞 NPS 4/2/2020
好的,虽然我承认在大多数情况下可能需要额外的变量,但我认为仍然有一些场景不需要它们(即使这种情况对任何实际情况都没有用)。就像在我提供的第二个示例中一样,我们知道一切都将按正确的顺序运行(除非以某种方式延迟超过 1 秒并开始等待,之后我不知道是否可能)。我的假设是正确的吗?mainTasknotify_one
0赞 Matt Timmermans 4/2/2020
当然,但是你怎么知道mainTask的延迟不超过一秒呢?依赖这样的东西是危险的
0赞 NPS 4/2/2020
确定。你能提供 2 的答案吗?和 3.在我的原始帖子中?
0赞 Matt Timmermans 4/2/2020
当然,没问题
0赞 NPS 4/3/2020
API 难道不需要任何类型的变量(使用模板或其他东西)吗?
0赞 Andreas 2/11/2022 #2

是的,条件变量只对等待事件有用。在我看来,您不应该尝试使用它来控制关键数据结构的并发访问。

我只能谈谈C++。正如您在此处 https://en.cppreference.com/w/cpp/thread/condition_variable/wait 示例中看到的,他们使用了以下表达式。并且是无名函数的表达式。所以你也可以编写自己的函数并给出函数的名称:cv.wait(lk, []{return i == 1;});[]{...}

bool condFn()
{
    std::cout << "condFn" << std::endl; // debug output ;)
    return i == 1;
}

void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cerr << "Waiting... \n";
    cv.wait(lk, condFn);
    std::cerr << "...finished waiting. i == 1\n";
}

在这个函数中,你可以评估,无论你想要什么。线程始终处于休眠状态,直到收到通知,然后始终处理评估继续工作条件的函数。如果为 true,线程继续,如果为 false,程序将再次进入睡眠状态。