为啥 pthread 的条件变量函数需要互斥锁?
Posted
技术标签:
【中文标题】为啥 pthread 的条件变量函数需要互斥锁?【英文标题】:Why do pthreads’ condition variable functions require a mutex?为什么 pthread 的条件变量函数需要互斥锁? 【发布时间】:2010-05-04 08:05:01 【问题描述】:我正在阅读pthread.h
;条件变量相关函数(如pthread_cond_wait(3)
)需要互斥体作为参数。为什么?据我所知,我将创建一个互斥体 just 用作该参数?那个互斥锁应该做什么?
【问题讨论】:
【参考方案1】:这只是条件变量被(或最初)实现的方式。
互斥锁用于保护条件变量本身。这就是为什么您需要在等待之前将其锁定。
等待将“原子地”解锁互斥锁,允许其他人访问条件变量(用于发出信号)。然后当条件变量发出信号或广播到时,等待列表中的一个或多个线程将被唤醒,互斥锁将再次神奇地为该线程锁定。
您通常会看到以下带有条件变量的操作,说明它们是如何工作的。下面的例子是一个工作线程,它通过一个信号给一个条件变量来工作。
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/mutex 对在这里受到互斥保护的——但是,其他线程需要在更改互斥之前锁定互斥)。
在技术上线程可以从条件等待中返回而不会被另一个进程踢(这是一个真正的虚假唤醒)但是,在我从事 pthread 的所有这些年中,无论是在代码的开发/服务,作为它们的用户,我从来没有收到过其中的一个。也许这只是因为惠普有一个不错的实施:-)
在任何情况下,处理错误情况的相同代码也处理真正的虚假唤醒,因为不会为这些设置工作可用标志。
【讨论】:
'do something' 不应该在 while 循环中。您希望您的 while 循环仅检查条件,否则如果您收到虚假唤醒,您也可能会“做某事”。 不,错误处理仅次于此。使用 pthreads,您可以在没有明显原因(虚假唤醒)的情况下被唤醒,并且不会出现任何错误。因此,您需要在醒来后重新检查“某些情况”。 也许我说得不够清楚。循环不是等待工作准备好,这样你就可以做到了。该循环是主要的“无限”工作循环。如果您从 cond_wait 返回并且设置了工作标志,则您完成工作然后再次循环。仅当您希望线程停止工作时,“虽然某些条件”才会为假,此时它将释放互斥锁并很可能退出。 @stefaanv "互斥体仍然是保护条件变量,没有其他方法可以保护它":互斥体不是保护条件变量;这是为了保护谓词数据,但我想你从阅读该声明之后的评论就知道了。您可以合法地发信号通知条件变量,并得到实现的完全支持,post-unlock 包装谓词的互斥锁,实际上您将减轻争用在某些情况下。 @WhozCraig,+1,是的,互斥锁不是为了保护条件变量。【参考方案2】:如果您只能发出条件信号,则条件变量是非常有限的,通常您需要处理一些与发出信号的条件相关的数据。信号/唤醒必须以原子方式完成,以在不引入竞争条件或过于复杂的情况下实现这一目标
出于技术原因,pthreads 还可以为您提供 spurious wakeup 。这意味着您需要检查谓词,这样您就可以确定条件实际上已发出信号 - 并将其与虚假唤醒区分开来。检查这样的条件以等待它需要被保护 - 因此条件变量需要一种在锁定/解锁保护该条件的互斥锁时自动等待/唤醒的方法。
考虑一个简单的示例,通知您生成了一些数据。也许另一个线程制作了一些您想要的数据,并设置了指向该数据的指针。
想象一个生产者线程通过“some_data”将一些数据提供给另一个消费者线程 指针。
while(1)
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
char *data = some_data;
some_data = NULL;
handle(data);
你自然会遇到很多竞争条件,如果另一个线程在你醒来后立即执行 some_data = new_data
,但在你之前执行 data = some_data
你也不能真正创建自己的互斥锁来保护这种情况。例如
while(1)
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
pthread_mutex_lock(&mutex);
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
不起作用,在唤醒和获取互斥锁之间仍有可能出现竞争条件。将互斥锁放在 pthread_cond_wait 之前对您没有帮助,就像您现在一样
等待时持有互斥锁 - 即生产者将永远无法获取互斥锁。
(请注意,在这种情况下,您可以创建第二个条件变量来向生产者表明您已完成 some_data
- 尽管这会变得复杂,尤其是如果您需要许多生产者/消费者。)
因此,您需要一种在等待/从条件唤醒时自动释放/获取互斥锁的方法。这就是 pthread 条件变量的作用,这就是你要做的:
while(1)
pthread_mutex_lock(&mutex);
while(some_data == NULL) // predicate to acccount for spurious wakeups,would also
// make it robust if there were several consumers
pthread_cond_wait(&cond,&mutex); //atomically lock/unlock mutex
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
(生产者自然需要采取相同的预防措施,始终使用相同的互斥锁保护“some_data”,并确保如果 some_data 当前为 != NULL 时它不会覆盖 some_data)
【讨论】:
不应该while (some_data != NULL)
是一个 do-while 循环,以便它至少等待条件变量一次吗?
没有。您真正在等待的是 'some_data' 为非空。如果“第一次”是非空的,很好,你持有互斥体并且可以安全地使用数据。如果您有一个 do/while 循环,如果有人在您等待条件变量之前发出信号,您将错过通知(这与在 win32 上发现的事件完全不同,它会一直发出信号直到有人等待它们)
我只是偶然发现了这个问题,坦率地说,奇怪的是发现这个正确的答案比 paxdiablo 有明显缺陷的答案少得多(仍然需要原子性,互斥锁仅用于处理条件,而不用于处理或通知)。我想这就是 *** 的工作原理......
@stefaanv,如果您想详细说明这些缺陷,作为我的答案,我会及时看到它们,而不是几个月后 :-),我很乐意修复他们。你的简短短语并没有给我足够的细节来弄清楚你想说什么。
@nos,while(some_data != NULL)
不应该是while(some_data == NULL)
吗?【参考方案3】:
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);
请注意,您需要互斥锁来保护工作队列。请注意,条件变量本身不知道是否有工作。也就是说,条件变量必须与条件相关联,该条件必须由您的代码维护,并且由于它在线程之间共享,因此它必须受到互斥锁的保护。
【讨论】:
或者,更简洁地说,条件变量的全部意义在于提供原子的“解锁并等待”操作。没有互斥锁,就没有什么可以解锁的。 您介意解释一下stateless的含义吗? @snr 他们没有任何状态。它们不是“锁定”或“发出信号”或“未发出信号”。因此,您有责任跟踪与条件变量相关的任何状态。例如,如果条件变量让线程知道队列何时变为非空,则必须是一个线程可以使队列变为非空,而其他线程需要知道队列何时变为非空。那是共享状态,您必须使用互斥锁来保护它。您可以将条件变量与受互斥锁保护的共享状态关联起来用作唤醒机制。【参考方案4】:并非所有条件变量函数都需要互斥锁:只有等待操作才需要。信号和广播操作不需要互斥体。条件变量也不是与特定互斥锁永久关联的;外部互斥锁不保护条件变量。如果条件变量具有内部状态,例如等待线程的队列,则必须由条件变量内部的内部锁保护。
等待操作将条件变量和互斥锁结合在一起,因为:
线程已锁定互斥体,对共享变量的某些表达式求值并发现它为假,因此需要等待。 线程必须原子地从拥有互斥锁转移到等待条件。出于这个原因,等待操作将互斥锁和条件作为参数:这样它就可以管理线程从拥有互斥锁到等待的原子传输,这样线程就不会成为的牺牲品失去唤醒比赛条件。
如果线程放弃互斥锁,然后等待无状态同步对象,则会发生丢失唤醒竞争条件,但以非原子的方式:存在线程不再拥有锁的时间窗口,并且尚未开始等待对象。在这个窗口期间,另一个线程可以进来,使等待的条件为真,发出无状态同步信号然后消失。无状态对象不记得它已发出信号(它是无状态的)。那么原来的线程在无状态同步对象上进入休眠状态,并且没有唤醒,即使它需要的条件已经变为真:丢失唤醒。
条件变量等待函数通过确保调用线程已注册以在它放弃互斥锁之前可靠地捕获唤醒来避免丢失唤醒。如果条件变量等待函数没有将互斥锁作为参数,这将是不可能的。
【讨论】:
您能否提供广播操作不需要获取互斥锁的参考?在 MSVC 上,广播被忽略。 @xvan POSIXpthread_cond_broadcast
和 pthread_cond_signal
操作(关于这个 SO 问题)甚至不将互斥锁作为参数;只有条件。 POSIX 规范是here。互斥锁仅在参考等待线程唤醒时发生的情况时提及。
您介意解释一下stateless的含义吗?
@snr 无状态同步对象不记得任何与信令相关的状态。发出信号时,如果现在有东西在等待它,则将其唤醒,否则将忘记唤醒。条件变量像这样是无状态的。根据正确编写的逻辑,使同步可靠的必要状态由应用程序维护并由与条件变量结合使用的互斥锁保护。【参考方案5】:
我没有发现其他答案像this page 那样简洁易读。通常等待的代码是这样的:
mutex.lock()
while(!check())
condition.wait(mutex) # atomically unlocks mutex and sleeps. Calls
# mutex.lock() once the thread wakes up.
mutex.unlock()
将wait()
包装在互斥体中的三个原因:
-
如果没有互斥体,另一个线程可以在
signal()
之前 wait()
,我们会错过这个唤醒。
通常check()
依赖于另一个线程的修改,所以无论如何你都需要互斥。
确保优先级最高的线程首先执行(互斥体的队列允许调度程序决定谁接下来执行)。
第三点并不总是一个问题 - 历史背景从文章链接到this conversation。
关于这种机制,经常提到虚假唤醒(即等待线程在没有调用 signal()
的情况下被唤醒)。但是,此类事件由循环的check()
处理。
【讨论】:
【参考方案6】:条件变量与互斥体相关联,因为它是唯一可以避免它旨在避免的竞争的方法。
// incorrect usage:
// thread 1:
while (notDone)
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
pthread_mutex_unlock(&mutex);
if (ready)
doWork();
else
pthread_cond_wait(&cond1); // invalid syntax: this SHOULD have a mutex
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
Now, lets look at a particularly nasty interleaving of these operations
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable;
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
if (ready)
pthread_cond_wait(&cond1); // uh o!
此时,没有线程会向条件变量发出信号,因此 thread1 将永远等待,即使 protectedReadyToRunVariable 表示它已准备就绪!
解决这个问题的唯一方法是让条件变量原子地释放互斥锁,同时开始等待条件变量。这就是 cond_wait 函数需要互斥体的原因
// correct usage:
// thread 1:
while (notDone)
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
if (ready)
pthread_mutex_unlock(&mutex);
doWork();
else
pthread_cond_wait(&mutex, &cond1);
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_cond_signal(&mutex, &cond1);
pthread_mutex_unlock(&mutex);
【讨论】:
【参考方案7】:当您调用pthread_cond_wait
时,互斥锁应该被锁定;当您以原子方式调用它时,它都会解锁互斥锁,然后在条件下阻塞。一旦条件发出信号,它就会再次原子地锁定它并返回。
如果需要,这允许实现可预测的调度,因为执行信号发送的线程可以等到互斥锁被释放后再进行处理,然后发出条件信号。
【讨论】:
那么……我是否有理由不让互斥锁始终处于解锁状态,然后在等待之前将其锁定,然后在等待结束后立即解锁? 互斥锁还解决了等待线程和信号线程之间的一些潜在竞争。只要在更改条件和信号时始终锁定互斥锁,您就永远不会发现自己错过信号并永远沉睡 所以……我应该首先在条件变量的互斥体上等待互斥体,然后再等待条件变量?我不确定我是否完全理解。 @elliottcable:不持有互斥锁,你怎么知道你应该等待还是不应该等待?如果您等待的刚刚发生了怎么办?【参考方案8】:这似乎是一个具体的设计决策,而不是概念上的需要。
根据 pthreads 文档,互斥锁未分离的原因是通过组合它们可以显着提高性能,并且他们期望由于常见的竞争条件,如果您不使用互斥锁,它几乎总是会无论如何都完成了。
https://linux.die.net/man/3/pthread_cond_wait
互斥锁和条件变量的特点
有人建议互斥量的获取和释放是 与条件等待分离。这被拒绝了,因为它是 操作的综合性质,事实上,有助于实时 实施。这些实现可以原子地移动一个 条件变量和互斥体之间的高优先级线程 对调用者透明的方式。这可以防止额外的 上下文切换并提供更确定性的互斥体获取 当等待线程发出信号时。因此,公平和优先 问题可以直接由调度规程处理。 此外,当前条件等待操作匹配现有 练习。
【讨论】:
【参考方案9】:对此有大量的注释,但我想用下面的例子来概括它。
1 void thr_child()
2 done = 1;
3 pthread_cond_signal(&c);
4
5 void thr_parent()
6 if (done == 0)
7 pthread_cond_wait(&c);
8
代码 sn-p 有什么问题?在继续之前稍微考虑一下。
这个问题真的很微妙。如果父母调用
thr_parent()
然后审核done
的值,它会看到它是0
和
因此尝试去睡觉。但就在它调用等待入睡之前,父母
在 6-7 行之间被打断,孩子跑了。孩子改变状态变量
done
到 1
和信号,但没有线程在等待,因此没有线程
醒来。当父级再次运行时,它永远处于休眠状态,这真是令人震惊。
如果它们是在单独获取锁时执行的呢?
【讨论】:
这才是真正的答案。【参考方案10】:如果你想要一个条件变量的真实例子,我在课堂上做了一个练习:
#include "stdio.h"
#include "stdlib.h"
#include "pthread.h"
#include "unistd.h"
int compteur = 0;
pthread_cond_t varCond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex_compteur;
void attenteSeuil(arg)
pthread_mutex_lock(&mutex_compteur);
while(compteur < 10)
printf("Compteur : %d<10 so i am waiting...\n", compteur);
pthread_cond_wait(&varCond, &mutex_compteur);
printf("I waited nicely and now the compteur = %d\n", compteur);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
void incrementCompteur(arg)
while(1)
pthread_mutex_lock(&mutex_compteur);
if(compteur == 10)
printf("Compteur = 10\n");
pthread_cond_signal(&varCond);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
else
printf("Compteur ++\n");
compteur++;
pthread_mutex_unlock(&mutex_compteur);
int main(int argc, char const *argv[])
int i;
pthread_t threads[2];
pthread_mutex_init(&mutex_compteur, NULL);
pthread_create(&threads[0], NULL, incrementCompteur, NULL);
pthread_create(&threads[1], NULL, attenteSeuil, NULL);
pthread_exit(NULL);
【讨论】:
以上是关于为啥 pthread 的条件变量函数需要互斥锁?的主要内容,如果未能解决你的问题,请参考以下文章