APUE
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
1. 创建和注销
条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
动态方式调用pthread_cond_init()函数,API定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)
尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。
注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API定义如下:
int pthread_cond_destroy(pthread_cond_t *cond)
2. 等待和激发
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)
等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
pthread_cond_wait() 用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。 pthread_cond_wait() 必须与pthread_mutex 配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread_cond_signal()或pthread_cond_broadcast,把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex。
激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。
现在来看一段典型的应用:看注释即可。
- #include <pthread.h>
- #include <unistd.h>
- static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
- static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- struct node {
- int n_number;
- struct node *n_next;
- } *head = NULL;
- /*[thread_func]*/
- static void cleanup_handler(void *arg)
- {
- printf("Cleanup handler of second thread./n");
- free(arg);
- (void)pthread_mutex_unlock(&mtx);
- }
- static void *thread_func(void *arg)
- {
- struct node *p = NULL;
- pthread_cleanup_push(cleanup_handler, p);
- while (1) {
- pthread_mutex_lock(&mtx); //这个mutex主要是用来保证pthread_cond_wait的并发性
- while (head == NULL) { //这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。这个时候,应该让线程继续进入pthread_cond_wait
- pthread_cond_wait(&cond, &mtx); // pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源
- //用这个流程是比较清楚的/*block-->unlock-->wait() return-->lock*/
- }
- p = head;
- head = head->n_next;
- printf("Got %d from front of queue/n", p->n_number);
- free(p);
- pthread_mutex_unlock(&mtx); //临界区数据操作完毕,释放互斥锁
- }
- pthread_cleanup_pop(0);
- return 0;
- }
- int main(void)
- {
- pthread_t tid;
- int i;
- struct node *p;
- pthread_create(&tid, NULL, thread_func, NULL); //子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大
- /*[tx6-main]*/
- for (i = 0; i < 10; i++) {
- p = malloc(sizeof(struct node));
- p->n_number = i;
- pthread_mutex_lock(&mtx); //需要操作head这个临界资源,先加锁,
- p->n_next = head;
- head = p;
- pthread_cond_signal(&cond);
- pthread_mutex_unlock(&mtx); //解锁
- sleep(1);
- }
- printf("thread 1 wanna end the line.So cancel thread 2./n");
- pthread_cancel(tid); //关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。关于取消点的信息,有兴趣可以google,这里不多说了
- pthread_join(tid, NULL);
- printf("All done -- exiting/n");
- return 0;
- }
当标志没有被设置的时候,线程会不断循环检测这个标志,同时会不断锁定、解锁互斥体,浪费 CPU 时间。你真正需要的是这样一种方法:当标志没有设置的时候让线程进入休眠状态;而当某种特定条件出现时,标志位被设置,线程被唤醒。
如同信号量,线程可以对一个条件变量执行等待操作。如果如果线程 A 正在等待一个条件变量,它会被阻塞直到另外一个线程B,向同一个条件变量发送信号以改变其状态。不同于信号量,条件变量没有计数值,也不占据内存空间,线程 A 必须在 B 发送信号之前开始等待。如果 B 在 A 执行等待操作之前发送了信号,这个信号就丢失了,同时 A会一直阻塞直到其它线程再次发送信号到这个条件变量。
条件变量将允许你实现这样的目的:在一种情况下令线程继续运行,而相反情况下令线程阻塞。只要每个可能涉及到改变状态的线程正确使用条件变量,Linux 将保证当条件改变的时候由于一个条件变量的状态被阻塞的线程均能够被激活。
GNU/Linux 刚好提供了这个机制,每个条件变量都必须与一个互斥体共同使用,以防止这种竞争状态的发生。这种设计下,线程函数应遵循以下步骤:
thread_function中的循环首先锁定互斥体并且读取标志变量的值。 如果标志变量已经被设定,该线程将互斥体解锁然后执行工作函数 如果标志没有被设置,该线程自动锁定互斥体并开始等待条件变量的信号
这里最关键的特点就在第三条。这里,GNU/Linux系统允许你用一个原子操作完成解除互斥体锁定和等待条件变量信号的过程而不会被其它线程在中途插入执行。这就避免了在thread_function中检测标志和等待条件变量的过程中其它线程修改标志变量并对条件变量发送信号的可能性。
pthread_cond_t pCond;
pthread_cond_init(&pCond,NULL); 第一个参数是一个指向pthread_cond_t变量的指针。第二个参数是一个指向条件变量属性对象的指针;这个参数在 GNU/Linux 系统中是被忽略的。
pthread_cond_signal(&pCond)如果没有线程正在等待这个信号,则这个信号会被忽略。该函数的
参数是一个指向 pthread_cond_t 类型变量的指针。
pthread_cond_broadcast()函数会将所有等待该条件变量的线程解锁而不是仅仅解锁一个线程
pthread_cond_wait(&pCond,&mutex)会让调用线程阻塞直到条件变量收到信号。第一个参数是指向一个 pthread_cond_t 类型变量的指针,第二个参数是指向一个pthread_mutex_t类型变量的指针。当调用 pthread_cond_wait 的时候,互斥体对象必须已经被调用线程锁定。这个函数以一个原子操作解锁互斥体并锁定条件变量等待信号。当信号到达且调用线程被解锁之后,pthread_cond_wait自动申请锁定互斥体对象。
pthread_mutex_lock(&qlock);
pthread_cond_wait(&qready, &qlock); //其它线程pthread_cond_sigal发出信号才会对出阻塞
pthread_mutex_unlock(&qlock);
The mutex passed to pthread_cond_wait protects the condition.The caller passes it locked to the function, which then atomically places the calling thread on the list of threads waiting for thecondition and unlocks the mutex. This closes the window between the time that the condition is checked and the time that the thread goes to sleep waiting for the condition to change, so that the thread doesn't miss a change in the condition. When pthread_cond_wait returns, the mutex is again locked.
上面是APUE的原话,就是说pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t*mutex)
函数传入的参数mutex用于保护条件,因为我们在调用pthread_cond_wait时,如果条件不成立我们就进入阻塞,但是进入阻塞这个期间,如果条件变量改变了的话,那我们就漏掉了这个条件。因为这个线程还没有放到等待队列上,所以调用pthread_cond_wait前要先锁互斥量,即调用pthread_mutex_lock(),pthread_cond_wait在把线程放进阻塞队列后,自动对mutex进行解锁,使得其它线程可以获得加锁的权利。这样其它线程才能对临界资源进行访问并在适当的时候唤醒这个阻塞的进程。当pthread_cond_wait返回的时候又自动给mutex加锁。
实际上边代码的加解锁过程如下:
pthread_mutex_lock(&qlock);
pthread_cond_wait(&qready, &qlock);
pthread_mutex_unlock(&qlock);
一般修改标志变量,经过以下步骤:
1. 锁定与条件变量伴生的互斥体。
2. 执行可能改变程序状态的指令(在我们的例子中,修改标志)。
3. 向条件变量投递或广播信号。这取决于我们希望的行为。
4. 将与条件变量伴生的互斥体解锁。
pCond.c
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
int flag;
pthread_cond_t pCond;
pthread_mutex_t mutex;
void initialize_flag()
{
pthread_mutex_init (&mutex, NULL);
pthread_cond_init (&pCond, NULL);
flag = 0;
}
void set_thread_flag (int flag_value)
{
flag = flag_value;
pthread_cond_signal (&pCond);
pthread_mutex_unlock (&mutex);
}
void* thread_fun1()
{
while (1)
{
pthread_mutex_lock (&mutex);
if (!flag)
pthread_cond_wait (&pCond, &mutex);
printf("thread 1 is running\n");
pthread_mutex_unlock (&mutex);
}
return NULL;
}
void *thread_fun2()
{
sleep(1);
printf("thread 2 is running\n");
set_thread_flag(1);
}
int main()
{
pthread_t id1,id2;
pthread_create(&id1,NULL,thread_fun1,NULL);
pthread_create(&id2,NULL,thread_fun2,NULL);
initialize_flag();
printf("main thread is running\n");
sleep(20);
return 0;
}
运行结果应为:
main thread is running
thread 2 is running
thread 1 is running
thread 1 is running
thread 1 is running
.......
因为线程1阻塞,至到线程2运行,给于信号后,线程1才开始运行
条件变量也可以用于不涉及程序状态的情况,而仅用作一种让一个线程阻塞等待其它线程唤醒的机制。信号量也可用于这个目的。两者之前的主要区别是,当没有线程处于阻塞状态的时候信号量会“记住”唤醒下一个被阻塞的线程,而条件变量只是简单地丢弃这个信号。另外,信号量只能发送一个唤醒信息给一个线程,而 pthread_cond_broadcast 可以同时唤醒不限数量的可以被唤醒的线程。