鉴于condition_variable是一个类的成员,我的理解是:
根据这些期望,我的问题是:为什么随机下面的示例代码无法通知等待的线程?
#include <mutex>
#include <condition_variable>
#define NOTIFY_IN_DESTRUCTOR
struct notify_on_delete {
std::condition_variable cv;
~notify_on_delete() {
#ifdef NOTIFY_IN_DESTRUCTOR
cv.notify_all();
#endif
}
};
int main () {
for (int trial = 0; trial < 10000; ++trial) {
notify_on_delete* nod = new notify_on_delete();
std::mutex flag;
bool kill = false;
std::thread run([nod, &flag, &kill] () {
std::unique_lock<std::mutex> lock(flag);
kill = true;
nod->cv.wait(lock);
});
while(true) {
std::unique_lock<std::mutex> lock(flag);
if (!kill) continue;
#ifdef NOTIFY_IN_DESTRUCTOR
delete nod;
#else
nod->cv.notify_all();
#endif
break;
}
run.join();
#ifndef NOTIFY_IN_DESTRUCTOR
delete nod;
#endif
}
return 0;
}
在上面的代码中,如果未定义NOTIFY_IN_DESTRUCTOR,则测试将可靠地运行至完成。但是,当定义NOTIFY_IN_DESTRUCTOR时,测试将随机挂起(通常在几千次试验后)。
我正在使用Apple Clang进行编译: Apple LLVM 9.0.0版(clang-900.0.39.2) 目标:x86_64-apple-darwin17.3.0 线程模型:posix 指定了C ++ 14,使用DEBUG标志设置编译。
修改
澄清:这个问题是关于condition_variable实例的指定行为的语义。上面的第二点似乎在以下quote中得到了加强:
块引用 要求:*此处不应阻止线程。 [注意:也就是说,所有线程都应该被通知;他们可能随后阻止等待中指定的锁定。这放宽了通常的规则,这将要求所有等待呼叫在破坏之前发生。只有取消阻止等待的通知才能在销毁之前发生。一旦析构函数启动,用户应该注意确保没有线程等待*,特别是当等待的线程在循环中调用wait函数或使用带谓词的wait,wait_for或wait_until的重载时。 - 结束说明]
核心语义问题似乎是“阻止”的意思。我现在对上面引用的解释是在行之后
cv.notify_all(); // defined NOTIFY_IN_DESTRUCTOR
在~inform_on_delete()中,线程测试不“阻塞”点头 - 也就是说我现在明白在这次调用之后“通知解锁等待”发生了,因此根据引用已满足要求以继续销毁condition_variable实例。
有人可以澄清“阻止”或“取消阻止通知”,结果是在上面的代码中,对notify_all()的调用不满足~condition_variable()的要求吗?
答案 0 :(得分:7)
定义NOTIFY_IN_DESTRUCTOR时:
调用notify_one()/notify_all()
并不意味着等待线程立即被唤醒,当前线程将等待另一个线程。它只是意味着如果等待线程在当前线程调用notify之后的某个时刻唤醒,它应该继续。所以从本质上讲,您可能会在等待线程唤醒之前删除条件变量(取决于线程的调度方式)。
为什么它挂起的解释,即使在另一个线程正在等待它时删除条件变量取决于使用与条件变量相关联的队列实现等待/通知操作的事实。这些队列使线程等待条件变量。释放条件变量意味着摆脱这些线程队列。
答案 1 :(得分:3)
我很确定您的供应商实施已经破裂。从遵守cv / mutex类的合同的角度来看,你的程序看起来几乎没问题。我无法100%验证,我支持一个版本。
“阻塞”的概念在condition_variable(CV)类中令人困惑,因为有多个东西需要阻塞。合同要求实现比pthread_cond *上的胶合板更复杂(例如)。我对它的解读表明,单个CV需要至少2个pthread_cond_t才能实现。
关键是具有定义的析构函数,而线程正在等待CV;它的废墟正处于CV.wait和~CV之间的竞争中。天真的实现简单地有~CV广播condvar然后消除它,并且让CV.wait记住局部变量中的锁,这样当它从运行时唤醒时,阻塞它的概念不再需要引用该对象。在该实现中,~CV成为“即发即忘”机制。
可悲的是,赛车CV.wait可以满足前提条件,但还没有完成与物体的交互,当~CV潜入并摧毁它时。要解决比赛,CV.wait和~CV需要相互排斥,因此CV至少需要一个私人互斥锁来解决比赛。
我们尚未完成。通常没有潜在的支持[例如。内核]用于“等待由锁控制的cv并在我被阻止时释放此另一个锁”的操作。我认为即使是posix人也发现太有趣了。因此,在我的简历中埋入互斥量是不够的,我实际上需要一种允许我处理其中事件的机制;因此在CV的实现中需要私有condvar。 Obligatory David Parnas meme
几乎没问题,因为正如Marek R指出的那样,你在依赖于一个阶级的破坏开始之后就依赖它了。不是cv / mutex类,你的notify_on_delete类。冲突有点学术性。我怀疑clang在控制转移到nod-&gt; cv.wait()之后依赖于nod保持有效;但大多数编译器供应商的真正客户是基准测试,而不是程序员。
正如一般说明的那样,多线程编程很难,而且现在已经达到了c ++线程模型的最高点,最好还是给它一两年的时间来解决问题。这份合同令人惊讶。当我第一次看到你的程序时,我想'呃,你无法破坏可以被访问的cv,因为RAII'。傻我。
Pthreads是另一个糟糕的线程API。至少它不会尝试超出范围,并且足够成熟,强大的测试套件可以使供应商保持一致。