48

pthread_cond_wait(&cond_t, &mutex);在我的程序中使用,我想知道为什么这个函数需要一个互斥变量作为第二个参数。

是否pthread_cond_wait()在开始时(执行开始)解锁互斥锁,pthread_cond_wait()然后在完成时(就在离开之前pthread_cond_wait())锁定?

4

3 回答 3

143

关于条件变量及其用法的主题有很多文本,所以我不会用大量丑陋的细节让你厌烦。它们存在的原因是允许您通知谓词状态的更改。以下是理解正确使用条件变量及其互斥关联的关键:

  • 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);
}

嗯,是的,它会“工作”,但也好不到哪里去。XXXXXYYYYY我们不拥有互斥锁之间的时间段(这没关系,因为我们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 编程的人的主要障碍,我觉得额外的时间/努力是值得的。我希望你能从中有所收获。

于 2013-02-17T19:30:28.300 回答
37

当第一个线程调用pthread_cond_wait(&cond_t, &mutex);它时,它会释放互斥锁并等待条件cond_t发出信号完成并且 mutex可用。

因此,当pthread_cond_signal在另一个线程中调用时,它不会“唤醒”等待中的线程。mutex必须先解锁,只有这样第一个线程才有机会获得锁,这意味着“一旦pthread_cond_wait互斥锁成功返回,就应该被锁定并归调用线程所有。”

于 2013-02-17T18:33:53.313 回答
6

是的,它解锁,等待条件满足,然后等待它可以重新获取传递的互斥锁。

于 2013-02-17T18:27:29.260 回答