我pthread_cond_wait(&cond_t, &mutex);
在我的程序中使用,我想知道为什么这个函数需要一个互斥变量作为第二个参数。
是否pthread_cond_wait()
在开始时(执行开始)解锁互斥锁,pthread_cond_wait()
然后在完成时(就在离开之前pthread_cond_wait()
)锁定?
关于条件变量及其用法的主题有很多文本,所以我不会用大量丑陋的细节让你厌烦。它们存在的原因是允许您通知谓词状态的更改。以下是理解正确使用条件变量及其互斥关联的关键:
pthread_cond_wait()
同时解锁互斥锁并开始等待条件变量发出信号。因此,在调用互斥锁之前,您必须始终拥有该互斥锁的所有权。
pthread_cond_wait()
与互斥锁一起返回,因此您必须解锁互斥锁以允许在完成后在其他地方使用它。是否因为条件变量发出信号而发生返回是不相关的。您仍然需要检查您的谓词,无论是否考虑潜在的虚假唤醒。
互斥锁的目的不是保护条件变量;它是为了保护条件变量被用作信号机制的谓词。这是 pthread 条件变量及其互斥锁最常被误解的习语。条件变量不需要互斥保护;谓词数据确实如此。将谓词视为条件变量/互斥锁对的用户正在监视的外部状态。
例如,等待布尔标志的一段微不足道但明显错误fSet
的代码:
bool fSet = false;
int WaitForTrue()
{
while (!fSet)
{
sleep(n);
}
}
我应该很明显,主要问题是谓词 ,fSet
根本不受保护。很多事情都可能在这里出错。例如:从您评估 while 条件到您开始等待(或旋转,或其他)的时间,值可能已经改变。如果不知何故错过了该更改通知,则您无需等待。
我们可以稍微改变一下,这样至少谓词会以某种方式受到保护。修改和评估谓词的互斥很容易通过(还有什么)互斥锁提供。
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
bool fSet = false;
int WaitForTrue()
{
pthread_mutex_lock(&mtx);
while (!fSet)
sleep(n);
pthread_mutex_unlock(&mtx);
}
好吧,这看起来很简单。现在我们永远不会在没有首先获得对它的独占访问权(通过锁定互斥锁)的情况下评估谓词。但这仍然是一个大问题。我们锁定了互斥体, 但在循环结束之前我们从不释放它。如果其他人都遵守规则并在评估或修改 之前等待互斥锁fSet
,那么在我们放弃互斥之前,他们永远无法这样做。在这种情况下,唯一能做到这一点的“人”是我们。
那么如何添加更多层呢。这行得通吗?
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
bool fSet = false;
int WaitForTrue()
{
pthread_mutex_lock(&mtx);
while (!fSet)
{
pthread_mutex_unlock(&mtx);
// XXXXX
sleep(n);
// YYYYY
pthread_mutex_lock(&mtx);
}
pthread_mutex_unlock(&mtx);
}
嗯,是的,它会“工作”,但也好不到哪里去。XXXXX
与YYYYY
我们不拥有互斥锁之间的时间段(这没关系,因为我们fSet
无论如何都不会检查或修改)。但是在此期间的任何时候,其他线程都可以(a)获得互斥锁,(b)修改,和(c)释放互斥锁,直到我们完成我们的,再次获得互斥fSet
锁,我们才会知道这件事sleep()
锁定,然后循环进行另一次检查。
必须有更好的方法。应该有一种方法可以释放互斥体并开始等待某种信号,告诉我们谓词可能发生了变化。同样重要的是,当我们收到该信号并返回我们的代码时,我们应该已经拥有授予我们检查谓词数据的访问权限的锁。这正是条件变量旨在提供的。
作用中的条件变量
输入条件变量 + 互斥锁对。互斥体保护对更改或检查谓词的访问,而条件变量建立了一个监视更改的系统,更重要的是,通过谓词互斥以原子方式(就您而言,无论如何)这样做:
int WaitForPredicate()
{
// lock mutex (means:lock access to the predicate)
pthread_mutex_lock(&mtx);
// we can safely check this, since no one else should be
// changing it unless they have the mutex, which they don't
// because we just locked it.
while (!predicate)
{
// predicate not met, so begin waiting for notification
// it has been changed *and* release access to change it
// to anyone wanting to by unlatching the mutex, doing
// both (start waiting and unlatching) atomically
pthread_cond_wait(&cv,&mtx);
// upon arriving here, the above returns with the mutex
// latched (we own it). The predicate *may* be true, and
// we'll be looping around to see if it is, but we can
// safely do so because we own the mutex coming out of
// the cv-wait call.
}
// we still own the mutex here. further, we have assessed the
// predicate is true (thus how we broke the loop).
// take whatever action needed.
// You *must* release the mutex before we leave. Remember, we
// still own it even after the code above.
pthread_mutex_unlock(&mtx);
}
对于其他一些线程来发出上面的循环信号,有几种方法可以做到,下面两种最流行:
pthread_mutex_lock(&mtx);
TODO: change predicate state here as needed.
pthread_mutex_unlock(&mtx);
pthread_cond_signal(&cv);
其他方式...
pthread_mutex_lock(&mtx);
TODO: change predicate state here as needed.
pthread_cond_signal(&cv);
pthread_mutex_unlock(&mtx);
每个人都有不同的内在行为,我邀请您针对这些差异做一些功课,并确定哪个更适合特定情况。前者以引入潜在的无根据的唤醒为代价提供了更好的程序流程。后者减少了这些唤醒,但代价是上下文协同作用减少。两种方法都可以在我们的示例中使用,您可以试验每种方法如何影响您的等待循环。无论如何,最重要的一点是,两种方法都可以完成此任务:
除非互斥锁被锁定,否则永远不要更改或检查谓词条件。曾经。
简单的监控线程
这种类型的操作在作用于特定谓词条件的监视器线程中很常见,它(没有错误检查)通常看起来像这样:
void* monitor_proc(void *pv)
{
// acquire mutex ownership
// (which means we own change-control to the predicate)
pthread_mutex_lock(&mtx);
// heading into monitor loop, we own the predicate mutex
while (true)
{
// safe to check; we own the mutex
while (!predicate)
pthread_cond_wait(&cv, &mtx);
// TODO: the cv has been signalled. our predicate data should include
// data to signal a break-state to exit this loop and finish the proc,
// as well as data that we may check for other processing.
}
// we still own the mutex. remember to release it on exit
pthread_mutex_unlock(&mtx);
return pv;
}
更复杂的监控线程
修改这个基本表单以考虑通知系统,一旦您收到通知,就不需要您保持互斥锁锁定,这会涉及更多一点,但不是很多。下面是一个监视器过程,一旦我们确定我们已经被服务(可以这么说),它就不会在常规处理期间保持互斥锁锁定。
void* monitor_proc(void *pv)
{
// acquire mutex ownership
// (which means we own change-control to the predicate)
pthread_mutex_lock(&mtx);
// heading into monitor loop, we own the predicate mutex
while (true)
{
// check predicate
while (!predicate)
pthread_cond_wait(&cv, &mtx);
// some state that is part of the predicate to
// inform us we're finished
if (break-state)
break;
// TODO: perform latch-required work here.
// unlatch the mutex to do our predicate-independant work.
pthread_mutex_unlock(&mtx);
// TODO: perform no-latch-required work here.
// re-latch mutex prior to heading into wait
pthread_mutex_lock(&mtx);
}
// we still own the mutex. remember to release it on exit
pthread_mutex_unlock(&mtx);
return pv;
}
有人会在哪里使用这样的东西?好吧,假设您的“谓词”是工作队列的“状态”以及一些告诉您停止循环并退出的标志。在收到“不同”的通知后,您检查是否应该继续执行循环,并决定是否应该继续,从队列中弹出一些数据。修改队列需要锁定互斥锁(记住,它的“状态”是我们谓词的一部分)。一旦我们弹出了我们的数据,我们就在本地拥有它并且可以独立于队列状态来处理它,所以我们释放互斥体,做我们的事情,然后需要互斥体进行下一次复飞。有很多方法可以对上述概念进行编码,包括明智地使用pthread_cond_broadcast
等。
事实证明这比我希望的要长得多,但这是学习 pthread 编程的人的主要障碍,我觉得额外的时间/努力是值得的。我希望你能从中有所收获。
当第一个线程调用pthread_cond_wait(&cond_t, &mutex);
它时,它会释放互斥锁并等待条件cond_t
发出信号完成并且 mutex
可用。
因此,当pthread_cond_signal
在另一个线程中调用时,它不会“唤醒”等待中的线程。mutex
必须先解锁,只有这样第一个线程才有机会获得锁,这意味着“一旦pthread_cond_wait
互斥锁成功返回,就应该被锁定并归调用线程所有。”
是的,它解锁,等待条件满足,然后等待它可以重新获取传递的互斥锁。