我正在阅读pthread.h;与条件变量相关的函数(如pthread_cond_wait(3))需要一个互斥量作为参数。为什么?据我所知,我要创建一个互斥量只是为了用作那个参数?互斥锁应该做什么?


当前回答

这只是条件变量(或最初)实现的方式。

互斥锁用于保护条件变量本身。这就是为什么你在等待之前要把它锁上。

等待将“原子地”解锁互斥锁,允许其他人访问条件变量(用于发送信号)。然后,当条件变量被发送信号或广播到时,等待列表上的一个或多个线程将被唤醒,互斥锁将神奇地再次为该线程锁定。

您通常会看到以下使用条件变量的操作,说明它们是如何工作的。下面的例子是一个工作线程,它通过信号将工作分配给一个条件变量。

thread:
    initialise.
    lock mutex.
    while thread not told to stop working:
        wait on condvar using mutex.
        if work is available to be done:
            do the work.
    unlock mutex.
    clean up.
    exit thread.

只要等待返回时有一些可用的工作,就在这个循环中完成。当线程被标记为停止工作时(通常是由另一个线程设置退出条件,然后启动条件变量来唤醒该线程),循环将退出,互斥锁将被解锁,该线程将退出。

上面的代码是一个单使用者模型,因为在工作完成时互斥锁保持锁定状态。对于多消费者的变化,您可以使用,作为一个例子:

thread:
    initialise.
    lock mutex.
    while thread not told to stop working:
        wait on condvar using mutex.
        if work is available to be done:
            copy work to thread local storage.
            unlock mutex.
            do the work.
            lock mutex.
    unlock mutex.
    clean up.
    exit thread.

它允许其他消费者在它做功的时候接收功。

条件变量减轻了轮询某些条件的负担,而不是允许另一个线程在需要发生某些事情时通知您。另一个线程可以告诉该线程工作可用,如下所示:

lock mutex.
flag work as available.
signal condition variable.
unlock mutex.

大多数通常被错误地称为虚假唤醒的情况通常都是因为在pthread_cond_wait调用(广播)中已经发出了多个线程的信号,其中一个线程会返回互斥锁,完成工作,然后重新等待。

然后,当没有工作要做的时候,第二条发出信号的线就可以出来了。所以你必须有一个额外的变量来指示应该做的工作(这是由condvar/互斥锁对固有的互斥锁保护的-其他线程需要在改变互斥锁之前锁定它)。

从技术上讲,线程从条件等待中返回而不被另一个进程踢出是可能的(这是一个真正的虚假唤醒),但是,在我多年的pthread工作中,无论是在代码的开发/服务还是作为它们的用户,我从来没有收到过这样的消息。也许这只是因为惠普有一个不错的实现:-)

在任何情况下,处理错误情况的相同代码也处理了真正的虚假唤醒,因为不会为它们设置工作可用标志。

其他回答

这只是条件变量(或最初)实现的方式。

互斥锁用于保护条件变量本身。这就是为什么你在等待之前要把它锁上。

等待将“原子地”解锁互斥锁,允许其他人访问条件变量(用于发送信号)。然后,当条件变量被发送信号或广播到时,等待列表上的一个或多个线程将被唤醒,互斥锁将神奇地再次为该线程锁定。

您通常会看到以下使用条件变量的操作,说明它们是如何工作的。下面的例子是一个工作线程,它通过信号将工作分配给一个条件变量。

thread:
    initialise.
    lock mutex.
    while thread not told to stop working:
        wait on condvar using mutex.
        if work is available to be done:
            do the work.
    unlock mutex.
    clean up.
    exit thread.

只要等待返回时有一些可用的工作,就在这个循环中完成。当线程被标记为停止工作时(通常是由另一个线程设置退出条件,然后启动条件变量来唤醒该线程),循环将退出,互斥锁将被解锁,该线程将退出。

上面的代码是一个单使用者模型,因为在工作完成时互斥锁保持锁定状态。对于多消费者的变化,您可以使用,作为一个例子:

thread:
    initialise.
    lock mutex.
    while thread not told to stop working:
        wait on condvar using mutex.
        if work is available to be done:
            copy work to thread local storage.
            unlock mutex.
            do the work.
            lock mutex.
    unlock mutex.
    clean up.
    exit thread.

它允许其他消费者在它做功的时候接收功。

条件变量减轻了轮询某些条件的负担,而不是允许另一个线程在需要发生某些事情时通知您。另一个线程可以告诉该线程工作可用,如下所示:

lock mutex.
flag work as available.
signal condition variable.
unlock mutex.

大多数通常被错误地称为虚假唤醒的情况通常都是因为在pthread_cond_wait调用(广播)中已经发出了多个线程的信号,其中一个线程会返回互斥锁,完成工作,然后重新等待。

然后,当没有工作要做的时候,第二条发出信号的线就可以出来了。所以你必须有一个额外的变量来指示应该做的工作(这是由condvar/互斥锁对固有的互斥锁保护的-其他线程需要在改变互斥锁之前锁定它)。

从技术上讲,线程从条件等待中返回而不被另一个进程踢出是可能的(这是一个真正的虚假唤醒),但是,在我多年的pthread工作中,无论是在代码的开发/服务还是作为它们的用户,我从来没有收到过这样的消息。也许这只是因为惠普有一个不错的实现:-)

在任何情况下,处理错误情况的相同代码也处理了真正的虚假唤醒,因为不会为它们设置工作可用标志。

当你调用pthread_cond_wait时,互斥锁应该是锁定的;当你调用它时,它会自动地解锁互斥锁,然后在条件上阻塞。一旦条件发出信号,它就会自动锁定它并返回。

这允许在需要的情况下实现可预测的调度,因为发送信号的线程可以等到互斥锁释放后再进行处理,然后发出条件信号。

我没有发现其他的答案像这一页一样简洁易读。通常等待代码看起来像这样:

mutex.lock()
while(!check())
    condition.wait(mutex) # atomically unlocks mutex and sleeps. Calls 
                          # mutex.lock() once the thread wakes up.
mutex.unlock()

将wait()包在互斥锁中有三个原因:

如果没有互斥,另一个线程可能会在wait()之前发出()信号,我们将错过这个唤醒。 通常check()依赖于来自另一个线程的修改,所以无论如何都需要对它进行互斥。 确保优先级最高的线程先进行(互斥锁的队列允许调度器决定谁下一个进行)。

第三点并不总是值得关注的——从文章到对话的历史背景是有联系的。

关于这种机制,经常提到虚假唤醒(即等待线程在没有调用signal()的情况下被唤醒)。但是,这样的事件由循环的check()处理。

POSIX条件变量是无状态的。所以维护国家是你的责任。由于等待的线程和通知其他线程停止等待的线程都将访问该状态,因此必须使用互斥锁来保护它。如果您认为可以在没有互斥的情况下使用条件变量,那么您没有理解条件变量是无状态的。

条件变量是围绕条件构建的。等待条件变量的线程是在等待某种条件。发出条件变量信号的线程会改变该条件。例如,一个线程可能正在等待一些数据到达。其他线程可能会注意到数据已经到达。“数据已到”是条件。

下面是条件变量的经典用法,简化后:

while(1)
{
    pthread_mutex_lock(&work_mutex);

    while (work_queue_empty())       // wait for work
       pthread_cond_wait(&work_cv, &work_mutex);

    work = get_work_from_queue();    // get work

    pthread_mutex_unlock(&work_mutex);

    do_work(work);                   // do that work
}

查看线程是如何等待工作的。工作由互斥锁保护。等待释放互斥量,以便另一个线程可以给这个线程一些工作。它是如何发出信号的:

void AssignWork(WorkItem work)
{
    pthread_mutex_lock(&work_mutex);

    add_work_to_queue(work);           // put work item on queue

    pthread_cond_signal(&work_cv);     // wake worker thread

    pthread_mutex_unlock(&work_mutex);
}

注意,您需要互斥来保护工作队列。注意,条件变量本身并不知道是否有功。也就是说,条件变量必须与条件相关联,该条件必须由代码维护,并且由于它在线程之间共享,因此必须使用互斥锁进行保护。

并不是所有的条件变量函数都需要互斥:只有等待操作需要。信号和广播操作不需要互斥。条件变量也不会永久地与特定的互斥锁相关联;外部互斥不保护条件变量。如果一个条件变量具有内部状态,例如等待线程的队列,则必须由条件变量中的内部锁保护。

等待操作将一个条件变量和一个互斥锁结合在一起,因为:

一个线程已经锁定了互斥锁,计算了共享变量的某个表达式,发现它为false,因此需要等待。 线程必须自动地从拥有互斥量转移到等待条件。

出于这个原因,等待操作将互斥量和条件作为参数:这样它就可以管理线程从拥有互斥量到等待的原子转移,这样线程就不会成为丢失唤醒竞争条件的受害者。

A lost wakeup race condition will occur if a thread gives up a mutex, and then waits on a stateless synchronization object, but in a way which is not atomic: there exists a window of time when the thread no longer has the lock, and has not yet begun waiting on the object. During this window, another thread can come in, make the awaited condition true, signal the stateless synchronization and then disappear. The stateless object doesn't remember that it was signaled (it is stateless). So then the original thread goes to sleep on the stateless synchronization object, and does not wake up, even though the condition it needs has already become true: lost wakeup.

条件变量等待函数通过确保调用线程在放弃互斥量之前被注册为可靠地捕获唤醒来避免丢失唤醒。如果条件变量等待函数不接受互斥量作为参数,这是不可能的。