条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起--消费者;另一个线程使"条件成立"(给出条件成立信号)--生产者。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
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_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;
而pthread_cond_broadcast()则激活所有等待线程,类似于观察者模式的NotifyAll。
3.一个百科上的生产者消费者例子进行分析
#include <pthread.h>
#include <unistd.h>
t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_
static pthread_mutex
_t cond = PTHREAD_COND_INITIALIZER;
struct node {
int n_number;
id cleanup_handler(v
struct node *n_next;
} *head = NULL;
/*[thread_func]*/
static v
ooid *arg)
{
printf("Cleanup handler of second thread./n");
free(arg);
struct node *p = NULL;
pthrea
(void)pthread_mutex_unlock(&mtx);
}
static void *thread_func(void *arg)
{
d_cleanup_push(cleanup_handler, p);
while (1) {
来保证pthread_cond_wait的并发性,多个生成者时保证互斥
while (head == NULL) { //这个while
pthread_mutex_lock(&mtx); //这个mutex主要是用要特别说明一下,单个pthread_cond_wait功能很完善,为何这里要有一个while (head == NULL)呢?因为pthread_cond_broadcast时,多个wait线程都被唤醒,这时假设只有一个head资源,且被第一个线程先获得,那么第一个将跳出while,直接消费掉。这时候轮到第二个线程,因为wait被唤醒时第一步就是先上锁,显然刚才锁被第一个率先获取。这时其实已经没有head资源了,如果没有while判断,线程2将继续往下运行,显然segment fault是必然的了。所以,这个时候,应该加入while判读,注意不是if判断让线程再次继续进入pthread_cond_wait。当然如果只有一个消费者,那么if也是可以的。
pthread_cond_wait(&cond, &mtx); // pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源
//用这个流程是比较清楚的/*block-->unlock-->wait() return-->lock*/
}
p = head;
tid, NULL, thread_func, NULL
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;
//signal和unlock顺序也是有讲究的
pthread_cond_signal(&cond)
;
pthread_mutex_unlock(&mtx); //解锁
sleep(1);
}
a end the line.So cancel thread 2./n");
pthread_cancel(tid);
printf("thread 1 wan
n //关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。关于取消点的信息,有兴趣可以google,这里不多说了
pthread_join(tid, NULL);
printf("All done -- exiting/n");
return 0;
}
调用pthread_cond_wait(&mycond,&mymutex),将自己阻塞起来,加入等待队列。
第一件事就是对互斥对象解锁,(你想啊,自己都要睡眠了,当然要把坑腾出来留给生产者啊,当然也可能被其他消费者使用),
现在互斥对象已经被解锁,其他线程就可以生成、消费链表头了。等待mycond是个阻塞操作,C1将睡眠,不占用CPU哦,
这正是我们期望的。
接下来假设P1锁定了mymutex并新生产了一个节点,P1接着调用pthread_cond_broadcast(&mycond),那么C1将被唤醒,
接着从wait处开始执行,这时wait内部将重新锁定mymutex,如果这时head!=null,那么C1开始真正的下面操作。
小结下当发起一个pthread_cond_wait之后,分解后,实际上是三个动作:
1、解锁
2、等待 当收到一个解除等待的信号(pthread_cond_signal或者pthread_cond_broad_cast)之后,pthread_cond_wait马上需要做的动作是:
3、上锁
好,有了上面的基础,我们可以说说while(NULL==head)的问题了。单个pthread_cond_wait功能很完善,这里为啥还要多出个判断呢?
因为broadcast时,多个wait线程都将被唤醒,假如现在情形是C1,C2都被唤醒,这是可能发生的,因为上面wait会先释放锁,
且这时只有一个head资源,被C1先获得,那么第一个while条件不满足直接跳出,直接下面消费掉head。这是轮到C2了,
因为wait被唤醒时第一步是上锁,显然刚才锁被C1拿到,这次才轮到C2,这时已经没有head资源了,如果没有while判断,
C2将直接向下运行,显然segment fault是必然的了。所以这时应该加入while判断,注意不是if,如果不满足资源再次进入pthread_cond_wait。
当然如果只有一个消费者,那么if判断也是可以的。
4.最后说下broadcast和unlock调用先后顺序问题:
broadcast先于unlock,这种情形下还没有释放mymutex锁,所以唤醒的线程无法获得锁(wait唤醒后首先获取锁,上面3),
将阻塞在获取锁处,一旦释放锁,就看RP了,谁先获取谁先消费。这是正确形式,mycond状态是预期的有效的。
unlock先于broadcast时,加入这时杀出C3,C3钻了空子head!=NULL,直接消费了head,然后C1、C2才被唤醒,
C1、C2还以为有资源了,这本来就是broadcast的预期效果,但此时mycond条件已经被修改为无效了,
虽然C1、C2会再次在while中进入wait,但这不是我们预期的。
--the end.