linux学习之旅(33) ---- 条件变量
条件变量使线程同步中一个很重要的概念,在之前的文章中我们也多次提及过。
条件变量
条件变量(cond)使在多线程程序中用来实现“等待--->唤醒”逻辑常用的方法,是进程间同步的一种机制。条件变量用来阻塞一个线程,直到条件满足被触发为止,通常情况下条件变量和互斥量同时使用。一般条件变量有两个状态:(1)一个/多个线程为等待“条件变量的条件成立“而挂起;(2)另一个线程在“条件变量条件成立时”通知其他线程。
为什么条件变量总是和互斥锁结合使用?
这其实有两方面的原因:
(1)互斥锁可以表示的状态的太少了,可以利用条件变量来增加有限的状态。
(2)条件变量虽然是线程同步的重要方法,但仅仅依靠条件变量是没有办法完成完成线程同步的工作的。
现在提出一个问题:
有两个线程,贡献一个全局变量count,count的初始值为0。这两个线程的任务是:线程1负责将count的的数值加到10,而线程而负责在线程1将count加到10之后将count输出后清零,这交替循环。
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
int count=0;
pthread_mutex_t myMutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t myCond=PTHREAD_COND_INITIALIZER;
void* threadHandle1(void* argv)
{
while(1)
{
pthread_mutex_lock(&myMutex);
++count;
pthread_mutex_unlock(&myMutex);
//留给其他线程足够的时间争用锁
sleep(1);
}
}
void* threadHandle2(void* argv)
{
while(1)
{
//为了保证在线程进入临界区是,count的数值不会被修变。
if(count==10)
{
pthread_mutex_lock(&myMutex);
if(count==10)
{
printf("%d\n",count);
count=0;
}
pthread_mutex_unlock(&myMutex);
}
printf("%d\n",count);
sleep(1);
}
}
int main()
{
pthread_t pid[2];
pthread_create(&pid[0],NULL,threadHandle1,NULL);
pthread_create(&pid[1],NULL,threadHandle2,NULL);
pthread_join(pid[0],NULL);
pthread_join(pid[1],NULL);
return 0;
}
虽然只是简单的两个线程对加法的运算,但线程1和线程2需要不停的交换锁的控制权,这样无疑就会给系统带来一些不必要的压力,原因是互斥锁只有两个状态(锁和不锁),而通过条件变量就会可以改进互斥锁在这一面的不足。
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
int count=0;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
void* threadHandle1(void* argv)
{
while(1)
{
pthread_mutex_lock(&mutex);
++count;
printf("thread1(mutex):count=%d\n",count);
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
if(count==5)
{
if(pthread_cond_signal(&cond)==0)
{
printf("thread1:(count=5)signal\n");
}
}
if(count>=10)
{
if(pthread_cond_signal(&cond)==0)
{
printf("thread1:(count=10)signal\n");
}
}
pthread_mutex_unlock(&mutex);
printf("thread1:(cond)unlock\n");
sleep(1);
}
}
void* threadHandle2(void* argv)
{
while(1)
{
pthread_mutex_lock(&mutex);
while(count<10)
{
//为什么使用while?
//防止signal唤醒的时机不对。
printf("thread2(while):count=%d\n",count);
//在函数返回之前将锁打开,在函数返回之后将锁关闭。
pthread_cond_wait(&cond,&mutex);
printf("condWait\n");
}
if(count>=10)
{
printf("thread2(if):count=%d\n",count);
count=0;
}
pthread_mutex_unlock(&mutex);
printf("mutexUnlock\n");
}
}
int main()
{
pthread_t pid[2];
pthread_create(&pid[0],NULL,threadHandle1,NULL);
sleep(1);
pthread_create(&pid[1],NULL,threadHandle2,NULL);
pthread_join(pid[0],NULL);
pthread_join(pid[1],NULL);
return 0;
}
代码解析:
pthread_cond_wait(&cond,&mutex);
该函数有三个作用:
(1)阻塞线程。
(2)将互斥锁加锁,并等待其他线程将其唤醒。(1)(2)为原子操作。
(3)在其他线程将其唤醒之后,将解锁的互斥锁重新加锁。
这里有两个问题:
(1)为什么要对线程2中的条件变量的部分加锁?
(2)在条件变量判断的时候为什么不用if而要使用while?
为什么要对线程2中的条件变量的部分加锁?
如果不加锁,在线程判断时假设这样一种情况:当线程1将count的数值加到9的时候,线程2去判断count的值,此时count的值还为9,那么线程2就会进入while循环中,等待线程1的条件成立,将自己唤醒。但就这这个时候,线程1还没有执行pthread_cond_wait时,线程1将count的值修改为10,并发送了signal信号,试图唤醒线程2。而线程2还没有执行wait所以并不会接收到这个信号,之后执行wait,而继续等待线程1的信号,但线程1会任务,自己已经将唤醒的信号发送了,这样就存在问题。
所以,需要在条件变量进行判断时,将变量锁住,让其他线程不能修改此变量,这样就可以保证在判断的时候条件的变量的值是正确的。即互斥锁的作用不是为了保护条件变量,而是为了保护条件判断时共享变量的值不会被修改。
在条件变量判断的时候为什么不用if而要使用while?
这个主要是为了防止其他线程在条件变量的条件还不成立的情况下,将睡眠中的线程错误的唤醒。
就像刚才的程序中的情况:我们的想法是在线程1将count的结果加到10时,将线程2唤醒,但线程1却在count等于5时将线程2唤醒,如果这里使用if就会出现问题。即程序不能保证signal线程将wait线程唤醒的时机时正确的,所以需要多重判断,就需要使用while,而不是使用if。
signal唤醒线程的时机
pthread_cond_signal(&cond);
通过上面的代码的结果分析,可以看出pthread_cond_signal的功能只是唤醒一个被条件变量阻塞的线程,但该函数不会修改锁的状态。而pthread_cond_wait会修改互斥锁的状态。
这里存在这样一个问题:(1)先解锁,再唤醒;(2)先唤醒,再解锁。因为wait再被唤醒会会有加锁操作。
(1)先解锁互斥锁,再唤醒睡眠的线程。
优点:减少了线程再内核态了用户态切换的次数,减少了资源的消耗。因为唤醒线程和解锁,都是需要再内核态完成的,而先解锁,再唤醒,内核会一次将这两个操作完成,这样就减少了用户态和内核态切换的次数,从而节省了资源。
缺点:如果此时存在一个低优先级的线程在等待锁,那么一旦锁被释放,那么这个锁就会被低优先级的线程争抢去,而不会被wait的线程得到,导致wait线程阻塞,无法返回。
(2)先唤醒睡眠的线程,再解锁互斥锁。
优点:唤醒后的线程在等待为该互斥锁加锁,一旦锁被释放,wait线程就会立即加锁,而不会发生上述,锁被抢占额度情况。
缺点:会增加用户态到内核态切换的次数,增加资源的消耗。
虽然在语法这两个都可以,但一般在程序使用先唤醒,再解锁的方式。