提问人:NPS 提问时间:3/30/2020 更新时间:2/11/2022 访问量:530
条件变量真的需要另一个变量吗?
Does a condition variable really need another variable?
问:
注意:我将用 C++ 举例,但我相信我的问题与语言无关。如果我错了,请纠正我。
只是为了让你真正理解我 - 我在这里想学习的是该工具的作用,仅此而已。不是它通常的用途,不是约定所说的,只是钝器的作用。在本例中 - 条件变量的作用。
到目前为止,在我看来,这是一种简单的机制,允许线程等待(阻塞)直到其他线程发出信号(取消阻塞它们)。仅此而已,无需处理关键部分访问或数据访问(当然,它们可以用于此,但这只是程序员的选择问题)。此外,信令通常只在重要事件发生时(例如加载数据)进行,但理论上它可以随时调用。到目前为止正确吗?
现在,我看到的每个示例都使用条件变量对象(例如)以及一些附加变量来标记是否发生了某些事情(例如)。看看 https://thispointer.com//c11-multithreading-part-7-condition-variables-explained/ 的这个例子:std::condition_variable
bool 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_condVar
bool m_bDataLoaded
mainTask
std::condition_variable m_condVar
bool m_bDataLoaded
bool 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;
}
- 现在我知道了虚假唤醒,仅凭它们就需要使用一个额外的变量。我的问题是——他们只是这样做的原因吗?如果它们没有发生,是否可以只使用条件变量而不使用任何其他变量(顺便说一句,这不会使“条件变量”这个名字用词不当)?
- 另一件事是 - 使用其他变量难道不是条件变量也需要互斥锁的唯一原因吗?如果不是,其他原因是什么?
- 如果需要额外的变量(出于虚假唤醒或其他原因),为什么 API 不需要它们(在第二个代码中,我不必使用它们来编译代码)?(我不知道在其他语言中是否相同,所以这个问题可能是特定于 C++ 的。
答:
这不仅仅是关于虚假的唤醒。
当您致电时,您如何知道您正在等待的情况尚未发生?m_condvar.wait
也许“loadData”已经在另一个线程中被调用了。当它调用时,什么也没发生,因为没有线程在等待。notify_one()
现在,如果你打电话,你将永远等待,因为没有什么会向你发出信号。condvar.wait
原始版本没有这个问题,因为:
- 如果为 false,则知道数据未加载,设置为 true 后,调用方将发出条件信号;
m_bDataLoaded
m_bDataLoaded
- 锁被持有,我们知道在释放之前不能在另一个线程中修改它;
m_bDataLoaded
condvar.wait
在释放锁之前,会将当前线程放入等待队列中,因此我们知道在开始等待后会将其设置为 true,因此在我们开始等待后也会被调用。m_bDataLoaded
notify_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();
}
这里我们需要同样的关键保证,即线程在释放锁之前开始等待,但我们等待的条件更复杂,涉及变量和单独的数据结构,并且有多种方法可以退出等待。
评论
mainTask
notify_one
是的,条件变量只对等待事件有用。在我看来,您不应该尝试使用它来控制关键数据结构的并发访问。
我只能谈谈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,程序将再次进入睡眠状态。
评论