条件变量:等待与信号发送
使用互斥锁虽然可以解决一些资源竞争的问题,但互斥锁只有两种状态(加锁和解锁),这限制了互斥锁的用途。
条件变量(条件锁)也可以解决线程同步和共享资源访问的问题,条件变量是对互斥锁的补充,它允许一个线程阻塞并等待另一个线程发送的信号,当收到信号时,阻塞的线程被唤醒并试图锁定与之相关的互斥锁。
条件变量初始化
条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
动态方式调用函数int pthread_cond_init,API定义如下:
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
条件变量的属性由参数attr指定,如果参数attr为NULL,那么就使用默认的属性设置。尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。多线程不能同时初始化一个条件变量,因为这是原子操作。如果函数调用成功,则返回0,并将新创建的条件变量的ID放在参数cond中。
解除条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
调用
destroy
函数解除条件变量并不会释放存储条件变量的内存空间。
条件变量阻塞(等待)
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 *abtime);
等待有两种方式:条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEDOUT,结束等待,其中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()前的加锁动作对应。阻塞时处于解锁状态。
激活
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_signal
函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程
,
使其脱离阻塞状态
,
继续执行,如果没有线程处在阻塞等待状态
,pthread_cond_signal
也会成功返回。
共享变量的状态改变必须遵守
lock/unlock
的规则:需要在同一互斥锁的保护下使用
pthread_cond_signal
(即
pthread_cond_wait
必须放在
pthread_mutex_lock
和
pthread_mutex_unlock
之间)否则条件变量可以在对关联条件变量的测试和
pthread_cond_wait
带来的阻塞之间获得信号,这将导致无限期的等待(死锁)。因为他要根据共享变量的状态来决定是否要等待,所以为了避免死锁,必须要在
lock/unlock
队中。
共享变量的状态改变必须遵守
lock/unlock
的规则:
pthread_cond_signal
即可以放在
pthread_mutex_lock
和
pthread_mutex_unlock
之间,也可以放在
pthread_mutex_lock
和
pthread_mutex_unlock
之后,但是各有优缺点。
若为前者,在某些线程的实现中,会造成等待线程从内核中唤醒(由于
cond_signal)
然后又回到内核空间(因为
cond_wait
返回后会有原子加锁的行为),所以一来一回会有性能的问题(上下文切换)。详细来说就是,当一个等待线程被唤醒的时候,它必须首先加锁互斥量(参见
pthread_cond_wait()
执行步骤)。如果线程被唤醒而此时通知线程任然锁住互斥量,则被唤醒线程会立刻阻塞在互斥量上,等待通知线程解锁该互斥量,引起线程的上下文切换。当通知线程解锁后,被唤醒线程继续获得锁,再一次的引起上下文切换。这样导致被唤醒线程不能顺利加锁,延长了加锁时间,加重了系统不必要的负担。但是在
LinuxThreads
或者
NPTL
里面,就不会有这个问题,因为在
Linux
线程中,有两个队列,分别是
cond_wait
队列和
mutex_lock
队列,
cond_signal
只是让线程从
cond_wait
队列移到
mutex_lock
队列,而不用返回到用户空间,不会有性能的损耗,因此
Linux
推荐这种形式。
而后者不会出现之前说的那个潜在的性能损耗,因为在
signal
之前就已经释放锁了。但如果
unlock
和
signal
之前,有个低优先级的线程正在
mutex
上等待的话,那么这个低优先级的线程就会抢占高优先级的线程(
cond_wait
的线程
)
。而且,假设而这在上面的放中间的模式下是不会出现的。
而对于
pthread_cond_broadcast
函数,它使所有由参数
cond
指向的条件变量阻塞的线程退出阻塞状态,如果没有阻塞线程,则函数无效。
实例代码如下
:
void *produce_cond(void *arg)
{
for(;;)
{
pthread_mutex_lock(&put.mutex);
if(put.nput >= nitems)
{
pthread_mutex_unlock(&put.mutex);
return NULL;
}
buff[put.nput]=put.nval;
put.nput++;
put.nval++;
pthread_mutex_unlock(&put.mutex);
pthread_mutex_lock(&nready.mutex);
if(nready.nready == 0)
printf("produce_cond nready==0 ");
pthread_cond_signal(&nready.cond);
nready.nready++;
pthread_mutex_unlock(&nready.mutex);
*((int *)arg)+=1;
}
}
void *consume_cond(void *arg)
{
int i;
for(i=0;i<3;i++)
{
pthread_mutex_lock(&nready.mutex);
printf("consume_cond nready =%d ",&nready);
while(nready.nready == 0)
pthread_cond_wait(&nready.cond,&nready.mutex);
nready.nready--;
pthread_mutex_unlock(&nready.mutex);
printf("consume_cond ");
if(buff[i]!=i)
{
printf("buff[%d]=%d ",i,buff[i]);
}
}
}
在
produce_cond
中给用来统计准备好由消费者处理的条目数的计数器
nready.nready
加一。在加
1
之前,如果该计数器的值为
0
,就调用
pthread_cond_signal
唤醒可能正在等待其值变为非零的任意消费者线程。该计数器是在生产者和消费者之间共享的。因此只有锁住与之关联的互斥所
nready.mutex
时才能访问它。
consume_cond
中消费者只是等待计数器
nready.nready
变为非零,既然该计数器是在所有生产者和消费者之间共享的。那么只有锁住与之关联的互斥锁时才能测试它的值。如果在锁住该互斥锁期间该计数器的值为
0
,我们就调用
pthread_cond_wait
进入睡眠,该函数原子地执行以下两个动作:
1
给互斥锁
nread.mutex
解锁
2
把调用线程投入睡眠,直到另外某个线程就本条件变量调用
pthread_cond_signal.
pthread_cond_wait
在返回前重新给互斥锁
nready.mutex
上锁。因此当它返回并且我们发现计数器
nready.nready
不为
0
时,我们就该把计数器减一,然后给该互斥锁解锁