在学习了线程之后,在上一博客中我模拟实现了生产者消费者模型,再模拟实现生产者消费者模型的时候我们都知道,若不加制约,可能生产者还没生产完消费者就来消费了,所以我们就需要互斥量和条件变量来进行制约。
而如何制约呢?莫非就是加个锁,当生产者要生产时先去申请这个锁,当申请到锁时,就去进行生产,当生产完了之后就去释放锁,然后消费者再去申请锁,然后再进行消费。接下来我们就来聊聊这个锁。
就目前我们所学的锁来说吧,锁分为好几种,一种是挂起等待锁,一种是自旋锁,一种是死锁,一种是读写锁。
挂起等待锁:当进程等待时间相对过长的时候,我们就把需要等待的进程挂起,然后当被挂起的进程要运行的条件满足之后,它就被唤醒,然后执行自己所要完成的任务,这个就叫做挂起等待锁。
自旋锁:自选锁是当一个进程要访问某一个进程时,那个进程此时带着锁,让该进程无法访问,然后该进程就采用轮询的方式一直尝试进行访问,直到另一个的锁释放,然后该进程就可以进行访问。自旋锁是因为进程等待的时间相对跟挂起唤醒的时间相差不多,挂起容易造成系统资源的浪费,以及死锁的产生。
上面我们提到了死锁这个概念,接下来我们就来学习一下死锁。
1、死锁的种种
(1)死锁的概念
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
(2)死锁产生的条件
死锁并不是所有的进程都会遇到的,死锁产生也是要在一定的条件下才会产生,下面是死锁产生的四个条件:
- 互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
- 不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
- 请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
- 循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。
以上给出了导致死锁的四个必要条件,只要系统发生死锁则以上四个条件至少有一个成立。事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生。
(3)死锁的预防
我们知道不管是在线程间还是进程间死锁都不是一定会发生的,我们知道了死锁的产生有四个条件,那么如果我们想预防死锁的话,就要从死锁产生的四个条件来着手,由于死锁产生的第一个条件互斥条件是无法避免的,所有我们只能通过对其他三个条件进行破坏来预防死锁。
- 破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
- 破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
- 破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
2、读写者模型
(1)读写锁
在实现读写者模型之前,我们先来学习一下读写锁。
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少,相对与写,它们读的好机会反而比写的机会高得多。通常来说,在读的过程中,往往伴随着查找的操作,中间耗时很长,给这种代码段加锁会极大的降低我们程序的效率,所以这就需要我们下面要将的读写锁(读写锁实际上是一种自旋锁)。
当前锁状态 | 读锁请求 | 写锁请求 |
无锁 | 可以 | 可以 |
读锁 | 可以 | 不可以 |
写锁 | 不可以 | 不可以 |
注意:写独占,读共享,写锁优先级高。
读写锁接口:
初始化:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
销毁:
int pthread_rwlock_destroy(pthraed_rwlock_t *rwlock);
加锁和解锁:
int pthread_rwlock_rdlock(pthraed_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthraed_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthraed_rwlock_t *rwlock);
(2)读写者模型实现
这个读写者模型我们也可以用前边我们学习消费者生产者模型里总结的“321原则”来描述。
在读写者模型里的‘321原则’解释就是:三种关系,两种角色,一个读写场所;这三种关系就是读者与读者之间共享关系或者说是没什么之间联系,写者与写者之间是互斥关系,读者与写者之间同步与互斥关系;两种角色就是读者与写者;一个读写场所;
在读写者模型中,读者对于数据是只访问而不拿走,而在消费者生产者模型中,消费者是将数据拿走,不给别人访问的机会。
一般读写锁有两种策略:一是读者优先,二是写者优先。
接下来我们就来实现一下读写者模型。(不给我们这是写者优先)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
int counter;
pthread_rwlock_t rwlock;
void *writer(void *arg)
{
int t;
int i=*(int*)arg;
free(arg);
while(1)
{
t=counter;
usleep(1000);
pthread_rwlock_wrlock(&rwlock); //加写锁
printf("writer:%d:%#x: counter=%d ++counter=%d\n",i,pthread_self(),t,++counter);
pthread_rwlock_unlock(&rwlock); //写锁取消
usleep(5000);
}
}
void *reader(void *arg)
{
int t;
int i=*(int*)arg;
free(arg);
while(1)
{
pthread_rwlock_rdlock(&rwlock); //读者加锁
printf("reader:%d:%#x:counter=%d\n",i,pthread_self(),counter);
pthread_rwlock_unlock(&rwlock);//读者解锁
usleep(900);
}
}
int main()
{
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock,NULL);
for(i=0;i<3;i++) //创建3个写者
{
int *p=(int *)malloc(sizeof(int));
*p=i;
pthread_create(&tid[i],NULL,writer,(void *)p);
}
for(i=0;i<5;i++) //创建5个读者
{
int *p=(int *)malloc(sizeof(int));
*p=i;
pthread_create(&tid[i+3],NULL,reader,(void *)p);
}
for(i=0;i<8;i++)
{
pthread_join(tid[i],NULL);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
运行结果如下:
以上就是死锁和读写者模型的模拟实现。