03Linux下C语言锁的学习之条件变量
1 为何要使用条件变量?
- 通过上两篇对互斥锁和读写锁的学习,我们知道锁是用来保证共享数据的访问混乱,实现多线程同步。
但是你有没有想过,当这个共享数据不是固定的一个,而是会变化的呢。何为变化,就是说不断有人生产数据,不断有人消费数据,也就是我们经典的生产者消费者模型。而非上两篇例子中共享数据为固定的一个全局共享变量。
按照我们前两篇的做法,各个线程使用锁把这个装着共享数据的容器(size不断变化)锁住,然后不断轮询访问这个容器(假设为队列),即类似下面的代码:
while(1){
pthread_mutex_lock(&mutex);
//访问队列的数据
pthread_mutex_unlock(&mutex);
}
- 这样做的办法不是说不行,但是如果你每个线程都这样做,此时你使用top命令可以看到,你的这个进程占用的cpu高达100%甚至以上。非常浪费CPU,有人可能说睡眠一下就行了,确实睡眠会降低CPU很大的使用率,会降低到与平时正常水平,可是这样的话,你这还叫多线程处理吗?每个线程睡眠几秒,是非常耗时了。例如你登个游戏,每个画面等多几秒,你消费者还愿意使用这个产品吗?肯定不会。
引用别人博主的图片:
博主博客如下,这个博主写得也不错的:
C++多线程并发(三)—线程同步之条件变量
- 所以此时条件变量就来了。条件变量就是干这个事情的,当没有共享数据消费的时候,它就阻塞等待别人的通知,不必while每秒轮询;当有数据消费的时候,别人就会通知他去消费,非常智能。
- 条件变量一般是(也可以说必须)依赖着锁一起工作的。条件变量和锁它们两个都会造成阻塞,这一点非常重要,下面的函数pthread_cond_wait()将会讲到。
2 与条件变量相关的函数
注意,这些函数都是LinuxC下的函数,与Windows无关,并且和C++关系不大,因为C++自己有自己更先进的条件变量使用,在后续我的C++博客将会讲到。
1)pthread_cond_init函数
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
/*
功能:初始化一个条件变量。
参1:条件变量。一般放在全局。
参2:条件变量属性。一般为NULL取默认属性。
此外,也可以使用静态初始化的方法,与调用该函数一样:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
*/
2)pthread_cond_destroy函数
int pthread_cond_destroy(pthread_cond_t *cond);
/*
功能:销毁一个条件变量。
参1:条件变量。
*/
3)pthread_cond_wait函数
这个函数非常重要,是锁和条件变量相关函数中最重要的函数。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
/*
功能:简单说,就是阻塞等待一个条件变量。但是实际上它内部有三个功能:
1)阻塞等待一个条件变量。
2)解锁。也就是说,调用该函数之前必须上锁。(注意:第一第二步为原子操作,即这两个步骤是顺序连续执行的,不可能再这个线程执行一个另一个线程执行一个。)
3)上锁。
解释上面每一个步骤的作用:
1)当线程没有被其它线程使用broadcast或者signal唤醒时,就会阻塞在条件变量。注意这里是条件变量。
2)由于已经阻塞在条件变量了,所以必须释放锁让其它线程操作,例如生产者存放数据需要获取锁才行。
3)当被唤醒的时候,该函数会重新上锁,以便恢复原来上锁的状态。并且确定有数据可以消费后,上锁的状态可以阻塞其它线程访问,防止数据混乱。这是锁的阻塞。与第一点条件变量的阻塞区分开来。
也就是说,该函数拥有两次阻塞的功能。条件变量的阻塞是防止CPU占有率过高,锁的阻塞是防止数据访问混乱。
参1:条件变量。
参2:互斥锁。
*/
4)pthread_cond_timedwait函数
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
/*
功能:与上面的一样,但是多了一个最大等待时间参数。当超过这个时间的阻塞,该函数直接返回。
首先通过参见man sem_timedwait函数,查看struct timespec的结构体如下。
struct timespec {
time_t tv_sec; // seconds秒
long tv_nsec; // nanosecondes纳秒
}
参3abstime:绝对时间,从1970年1月1日开始算。
如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 1970年1月1日 00:00:01秒(早已经过去)。
正确用法:
time_t cur = time(NULL); 获取从1970年1月1日开始算的当前时间,即与abstime同单位的绝对时间。
struct timespec t; 定义timespec结构体变量t。
t.tv_sec = cur+1; 定时1秒
pthread_cond_timedwait (&cond, &mutex, &t); 传参参APUE.11.6线程同步条件变量小节
在讲解setitimer函数时我们还提到另外一种时间类型:
struct timeval {
time_t tv_sec; // seconds秒
suseconds_t tv_usec; // microseconds微秒
};
*/
上面说的这么多,参3的传参就是必须加上1970年1月1日的时间就行。例如10s+time(NULL)就是10s后的不再阻塞。
5)pthread_cond_signal函数
int pthread_cond_signal(pthread_cond_t *cond);
/*
功能:唤醒至少一个阻塞在条件变量的线程。注意:不是唤醒一个。
参1:条件变量。
*/
6)pthread_cond_broadcast函数
int pthread_cond_broadcast(pthread_cond_t *cond);
/*
功能:唤醒所有阻塞在条件变量的线程。
参1:条件变量。
*/
3 总结
- 1)条件变量的使用一般(可以说必须)依赖锁。
- 2)必须了解pthread_cond_wait函数的三个功能和顺序。该函数可以让我们知道条件变量和锁都有阻塞功能。条件变量的阻塞是防止CPU占有率过高,锁的阻塞是防止数据访问混乱。