Linux线程同步
上一篇我们讲了Linux线程的互斥,利用锁的机制保证了线程的安全。本篇博客我们将为大家讲解Linux下的同步机制,因为时序问题,而导致程序异常,我们称之为竞争状态。在线程条件下这种问题也不难理解,而为了避免发生竞争状态称为同步。
同步的概念
我们上面说了为了避免发生竞争状态称为同步,可是这样说太过于生涩,我们这里来举一个生活中的例子。
- 学校中有一个单人的自习室,自习室门口挂着一把钥匙,有人来上自习时就把钥匙拿走,其他人也就无法进入教室,当你不使用自习室时将钥匙放回去,其他人即便可以入内。但是这天张三来上自习时,他每次自习2分钟就坐立不安,所以他便想把钥匙放回去离开,但是每当放钥匙时张三良心不安,所以每次放下去的一瞬间他又拿起钥匙回到教室,久而久之教室外的人没有一个可以自习,从而产生了饥饿问题。后来自习室新加了一条规定,当你将钥匙放回去时如果想接着自习就需要在教室外排队,从而保证每个人都有自习的机会。
从上面的例子我们发现,拿走钥匙不让其他人入内其实是一种互斥的机制,但是为了解决饥饿问题,后来加入了排队机制,这其实就是一种同步的机制。现在相信你对同步有了进一步的理解,我们可以认为,互斥机制是为了保证安全,同步机制为了保证合理性。
同步:在保证数据安全的前提下,让线程能够按某种特定的访问顺序访问临界资源,从而有效的避免饥饿问题,叫做同步
条件变量
同步的机制是为了保证我们访问数据的合理性顺序,从某种角度上来说,这种合理性还要保证不同线程访问共享资源时的效率。这又该怎么理解呢?同样我们使用上面的例子接着说
- 张三热爱自习,所以每次他都去的最早,所以当其他同学来的时候就要排队等待,这些等待的同学每隔几分钟就去看一次张三是否离开,但是张三经常自习很久,所以这些等待的同学有时候会浪费一整天时间。但是自习室后来又加入了通知的机制,当没有人时就会通知其他人,这样使同学们的效率大大提高。
所以就如同上面的例子,Linux下的通知机制引入了条件变量确保了同步的合理性和高效性。当一个线程互斥地访问某个变量,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量,下面我们来看看条件变量的函数,这些函数真的非常像互斥锁。
初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
//参数一:要初始化的条件变量 参数二:一般设置为NULL
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
条件变量等待
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
//参数一为要等待的条件变量,参数二为一把互斥锁,这把锁的用途后面讲解
唤醒条件变量
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒所有的条件变量,惊群问题
int pthread_cond_signal(pthread_cond_t *cond);
这些接口非常容易使用,如果有不懂的地方,读者可以自行查阅man手册,现在来看一个简单的条件变量使用例子:此程序r2中的signal函数每隔1秒唤醒一次r1函数,所以每秒打印一次活动。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1( void *arg )
{
while ( 1 ){
pthread_cond_wait(&cond, &mutex);
printf("活动\n");
}
}
void *r2(void *arg )
{
while ( 1 ) {
pthread_cond_signal(&cond);
sleep(1);
}
}
int main( void )
{
pthread_t t1, t2;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, r1, NULL);
pthread_create(&t2, NULL, r2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
下面我们着重谈一谈为什么pthread_cond_wait函数为什么参数需要传互斥锁:
pthread_mutex_lock(&mutex);
//进入临界区访问临界资源
while (condition_is_false) {
pthread_cond_wait(&cond);
}
pthread_mutex_unlock(&mutex);
从上面代码中我们发现,当一个线程(可能是读线程)进入临界区时发现此时临界资源不具备读的条件,所以此时就需要另外一个线程对临界资源进行写操作,但是由于我们现在的读线程手中拿着锁,所以写线程拿不到锁,也就无法进入临界区。为了解决这一问题pthread_cond_wait函数在进行等待时会将锁自动释放,然后挂起。值得注意的是,挂起和等待这俩步操作合成了一个原子操作。当满足条件被唤醒时,此时读线程同时又获得了互斥锁。
有读者不理解为什么等待和释放锁需要是原子呢,那我们不如举一个反例,看能不能不是原子的:
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
由于解锁和等待不是原子操作。调用解锁之后,pthread_ cond_ wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_ cond_ wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_ cond_ wait。所以解锁和等待必须是一个原子操作。
总结
本节很简单,读者只要清楚理解什么是同步机制,同步机制为了解决什么问题即可。清楚条件变量从某种程度上是为了解决同步的效率问题,理解为什么条件变量的等待函数需要使用一把互斥锁。下篇博客我们就要使用条件变量来实现经典的生产者消费者模型。