为什么pthreads的条件variables函数需要一个互斥量?
我在读pthread.h
; 与条件variables相关的函数(如pthread_cond_wait(3)
)需要一个互斥量作为参数。 为什么? 据我所知,我将创build一个互斥体来作为这个参数? 那个互斥体应该做什么?
这只是条件variables(或最初)被执行的方式。
互斥量用于保护条件variables本身 。 这就是为什么你需要locking,然后再等待。
等待将“primefaces地”解锁互斥体,允许其他人访问条件variables(用于信号)。 然后,当条件variables被发送或广播时,等待列表中的一个或多个线程将被唤醒,并且该线程的互斥体将被再次神奇地locking。
您通常会看到以下操作和条件variables,说明它们如何工作。 下面的例子是一个工作线程,它通过一个信号给一个条件variables。
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.
这个工作是在这个循环内完成的,只要等待返回时有一些可用。 当线程被标记为停止工作(通常由另一个线程设置退出条件,然后踢条件variables来唤醒该线程),循环将退出,互斥体将被解锁,并且该线程将退出。
上面的代码是单用户模型,因为在工作完成时互斥锁保持locking状态。 对于多用户版本,您可以使用, 例如 :
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.
这允许其他消费者在这个人正在工作时接受工作。
条件variables可以减轻轮询某些条件的负担,而是允许另一个线程在需要发生的情况下通知您。 另一个线程可以告诉那个工作线程可用,如下所示:
lock mutex. flag work as available. signal condition variable. unlock mutex.
绝大多数通常被错误地称为虚假唤醒的东西通常总是因为多个线程在它们的pthread_cond_wait
调用(广播)中被发信号,一个会用互斥量返回,完成工作然后再等待。
那么当没有工作要做时,第二条信号线就会出来。 所以你必须有一个额外的variables表明工作应该完成(这是天生的互斥体保护与condvar /互斥对这里 – 其他线程需要locking互斥之前,改变它)。
从技术上讲,线程可以从一个等待状态中返回而不被另一个进程踢掉(这是一个真正的虚假唤醒),但是在我多年来从事pthread的工作中,无论是在代码的开发/服务中,还是作为用户其中,我从来没有收到过其中之一。 也许这只是因为惠普有一个体面的实施:-)
在任何情况下,处理错误情况的相同代码也会处理真正的虚假唤醒,因为不会为这些标志设置可用标志。
如果只能表示一个条件,那么条件variables是相当有限的,通常您需要处理一些与发送信号有关的数据。 信号/唤醒必须以primefaces方式完成,而不是引入竞争条件或过于复杂
由于技术上的原因,pthreads也可以给你一个虚假的唤醒 。 这意味着你需要检查一个谓词,所以你可以确定这个条件实际上是被标记出来的 – 并且从一个虚假的唤醒中区分出来。 检查这样一个等待它的条件需要保护 – 所以一个条件variables需要一个方法来自动等待/唤醒,同时locking/解锁一个守护那个条件的互斥锁。
考虑一个简单的例子,在这个例子中你会得到一些数据的产生。 也许另一个线程创build了一些你想要的数据,并设置一个指向这些数据的指针。
设想一个生产者线程通过'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
你不能真正创build自己的互斥体来防范这种情况
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之前放置这个互斥对你没有任何帮助,因为你现在将在等待的时候持有这个互斥 – 也就是说,制作者将永远无法获得这个互斥。 (注意,在这种情况下,你可以创build第二个条件variables来告诉生产者你已经完成了some_data
– 虽然这将变得复杂,尤其是如果你想要很多生产者/消费者的话)。
因此,您需要一种方法来在等待/从状态唤醒时自动释放/获取互斥锁。 这是pthread条件variables的作用,以下是你要做的事情:
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(如果some_data当前为!= NULL)
POSIX条件variables是无状态的。 所以维护国家是你的责任。 由于状态将被等待的线程以及告诉其他线程停止等待的线程访问,所以它必须由互斥体保护。 如果你认为你可以使用没有互斥的条件variables,那么你还没有掌握到条件variables是无状态的。
条件variables是围绕一个条件build立的。 在条件variables上等待的线程正在等待一些条件。 表示条件variables的线程改变了这个条件。 例如,一个线程可能正在等待一些数据到达。 其他一些线程可能会注意到数据已经到达。 “数据已到”是条件。
以下是条件variables的经典用法,简化了:
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); }
请注意,您需要使用互斥锁来保护工作队列。 请注意,条件variables本身不知道是否有工作。 也就是说,一个条件variables必须与一个条件相关联,该条件必须由您的代码维护,并且由于它在线程之间共享,所以它必须受互斥体保护。
并不是所有的条件variables函数都需要一个互斥锁:只有等待操作才能完成。 信号和广播操作不需要互斥。 一个条件variables也不是与一个特定的互斥锁永久关联; 外部互斥锁不保护条件variables。 如果一个条件variables具有内部状态,比如等待线程的队列,那么这个variables必须由条件variables内部的一个锁来保护。
等待操作将条件variables和互斥锁结合在一起,因为:
- 一个线程已经locking了互斥锁,通过共享variables评估了一些expression式,发现它是错误的,所以需要等待。
- 线程必须自动地从拥有互斥体移动到等待状态。
由于这个原因,等待操作既作为互斥体又作为parameter passing参数,以便它可以pipe理线程从拥有互斥体到等待的primefaces转移,以便线程不会成为丢失的唤醒竞争条件的牺牲品。
如果一个线程放弃了一个互斥锁,然后等待一个无状态的同步对象,但是以一种非primefaces的方式,将会出现一个失去的唤醒竞态条件:当线程不再有锁时存在一个时间窗口,尚未开始等待对象。 在此窗口期间,另一个线程可以进入,使等待的状态成为真实,发出无状态的同步信号,然后消失。 无状态的对象不记得它被标记(这是无状态的)。 因此,原始线程在无状态同步对象上进入睡眠状态,即使它所需要的条件已经变为真,唤醒也不会唤醒。
条件variables等待函数通过确保调用线程被注册以在放弃互斥体之前可靠地捕获唤醒来避免失去唤醒。 如果条件variableswait函数没有把互斥体作为参数,这将是不可能的。
当您调用pthread_cond_wait
时,互斥锁应该被locking; 当你调用它时,它会自动解锁互斥锁,然后阻止条件。 一旦条件发出信号,就会自动locking并返回。
如果需要的话,这允许执行可预测的调度,因为执行信令的线程可以等待,直到互斥量被释放以进行处理,然后发信号通知条件。
条件variables与互斥体相关联,因为它是避免它旨在避免的竞争的唯一方式。
// 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!
在这一点上,没有线程正在发送条件variables的信号,所以即使protectedReadyToRunVariable表明它已经准备好了,thread1也会永远等待。
唯一的解决方法是条件variables自动释放互斥量,同时开始等待条件variables。 这就是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);
我没有发现其他答案像这个页面一样简洁易读。 通常等待的代码看起来像这样:
mutex.lock() while(!check()) condition.wait() mutex.unlock()
有三个理由将wait()
包装在互斥体中:
- 如果没有互斥锁,另一个线程可能会在
wait()
之前signal()
wait()
,我们会错过这个唤醒。 - 通常
check()
依赖于来自另一个线程的修改,所以无论如何你需要相互排斥。 - 以确保最高优先级的线程首先进行(互斥队列允许调度程序决定接下来的人)。
第三点并不总是一个问题 – 历史背景是从文章与这个谈话联系起来的。
虚假的唤醒通常被提到关于这个机制(即等待的线程被唤醒而没有signal()
被调用)。 但是,这样的事件是由循环check()
处理的。
如果你想要一个条件variables的实例,我在课堂上做了一个练习:
#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); }
这似乎是一个具体的devise决定,而不是概念上的需要。
根据pthreads文档,互斥体没有分开的原因是通过将它们结合在一起会有显着的性能改进,并且他们期望如果不使用互斥体,由于共同的竞争条件,反正几乎总是要做的。
https://linux.die.net/man/3/pthread_cond_wait
互斥和条件variables的特点
有人build议,互斥的获取和释放与状态等待解耦。 这被拒绝了,因为这是操作的组合性质,事实上,这有助于实时实现。 那些实现可以以对调用者透明的方式primefaces地移动条件variables和互斥体之间的高优先级线程。 这可以防止额外的上下文切换,并在等待线程发信号时提供更确定性的互斥量采集。 因此,公平性和优先性问题可以由调度规则直接处理。 而且,目前的状况等待操作与现有的做法相符。