线程间的互斥
为什么会出现互斥?
- 线程是在线程栈上开辟的,而一个进程的线程是共享该进程的地址空间。
- 而在线程中使用的变量的地址空间都在线程栈的空间内,这个时候,变量归属于单个线程,而其他的变量无法获得这个变量。
- 但是很多时候需要线程间的变量的共享,这样的变量称为共享变量,可以通过数据的共享,完后多个进程之间的交互。
- 但是多个线程并发操作共享变量,会带来一些问题,这时候就引出了互斥量。
举一个简单的售票系统的例来体现
#include"stdio.h"
#include"stdlib.h"
#include"string.h"
#include"unistd.h"
#include"pthread.h //线程所需要的头文件
int ticket=100;
void *route(void*arg)
{
char *id=(char*)arg;
while(1){
if(ticket>0){ //没有对这块代码进行锁定,那么线程都可以同时访问。
printf("%s sells ticket:%d\n",id,ticket);
ticket--;
}
else{
break;
}
}
}
int main(void)
{
pthread_t t1,t2,t3;
pthread_create(&t1,NULL,route,"thread 1");
pthread_create(&t2,NULL,route,"thread 1");
pthread_create(&t3,NULL,route,"thread 1");
pthread_join(t1,NULL);//线程的等待,防止退出的线程空间没有释放
pthread_join(t2,NULL);
pthread_join(t3,NULL);
}
//以上的程序没有定义互斥量,当两个客户同时在买票,即同时有两个线程要同时进入这个售票的代码区。
解决方法
- 那么依照我们的日常生活的常识,当一个人在买火车票的时候,进行支付的时候其他的客户在和你同一时刻来买票的时候只能有一个人可以进入售票的代码区,进入之后,就锁定了,其他人无法执行。这时候需要一个锁来实现这一功能,就是互斥量。
那么对以上的代码进行一个添加互斥量的优化。
#include"stdio.h"
#include"stdlib.h"
#include"string.h"
#include"unistd.h"
#include"pthread.h //线程所需要的头文件
#include"sched.h" //用互斥量所需要引进的头文件
int ticket=100;//共享变量
pthread_mutex_t mutex; //互斥量的定义
void *route(void*arg)
{
char *id=(char*)arg;//在上锁之前的代码块是共享的,比如都可以进入选座的代码区
while(1){
pthread_mutex_lock(&mutex);//上锁,只允许一个线程进入,其他线程
if(ticket>0){ //如果还有票
usleep(1000);
printf("%s sells ticket:%d\n",id,ticket);
ticket--;
pthread_unlock(&mutex); //系统对售票做出了回应,那么就开锁
}
else{ //如果没有票了,大家也不会因为同时买票而出现问题
pthread_mutex_unlock(&mutex);//所以打开锁
break; //直接跳出循环
}
}
}
int main(void)
{
pthread_t t1,t2,t3; //多个线程,代表给多个用户售票
pthread_mutex_init(&mutex,NULL); //初始化互斥量,属于动态分配
pthread_create(&t1,NULL,route,"thread 1");
pthread_create(&t2,NULL,route,"thread 1");
pthread_create(&t3,NULL,route,"thread 1");
pthread_join(t1,NULL);//线程的等待,防止退出的线程空间没有释放
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_mutex_destory(&mutex);//在使用完之后要对动态分配的互斥量进行销毁,已经销毁的互斥量要保证后面不会再尝试加锁,否则会造成死锁。
}
- 但是互斥锁的使用不当会引起一些问题,比如最经典的死锁问题。
什么死锁?如何避免?
- 两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的情况,而这些在互相等待的进程称为死锁进程。
- 死锁的必要条件: 互斥(两个线程同时需求同一份资源);请求和保持(已经拥有了一个锁还请求另一个):不剥夺(进程已经获得的资源在结束之前不被剥夺;环路等待条件(发生死锁的时候,必须要有进程资源的环路关系链)
- 如何解决?预防死锁;避免死锁(比较经典的是银行家算法,哲学家就餐);检测死锁;解决死锁。
条件变量的引入
- 而在上面的例子中我们可以想到,在一个用户等待另一个用户买票的和过程中,他什么都做不了, 这种时候就需要条件变量。
- 条件变量允许线程阻塞和等待线程发送信号来唤醒。在一定程度上弥补了互斥锁只有打开和关闭两种状态的不足,所以它和互斥锁一起合作使用。
- 条件变量被用来阻塞一个线程,当条件不满足时,线程打开互斥锁。,线程一直等待。一旦其他的线程退出,这时候满足条件了,然后就有信号来唤醒另一个线程。
- 条件变量被用来进行线程间的同步,提高了进程和线程的运行效率。
来看一个例子:
#include"stdio.h"
#include"stdlib.h"
#include"string.h"
#include"unistd.h"
#include"pthread.h //线程所需要的头文件
#include"sched.h" //用互斥量所需要引进的头文件
pthrad_con_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_mutex_init(&mutex,NULL); //初始化互斥量
pthread_cond_init(&cond,NULL); //初始化条件变量
pthread_create(&t1,NULL,r1,NULL);
pthread_create(&t2,NULL,r2,NULL);
pthread_join(t1,NULL);//线程的等待,防止退出的线程空间没有释放
pthread_join(t2,NULL);
pthread_cond_destroy(&cond); //条件变量的销毁
pthread_mutex_destroy(&mutex);//互斥量的销毁
为什么等待条件满足的时候需要互斥量?
- 假如同时有两个线程。线程1调用了pthread_cond_wait()函数处于等待的状态,此时线程2调用了 cond_singal 函数唤醒线程1,但是线程1的条件还没有满足,它还不能够被唤醒,这个时候cond_singal就丢失了。但是如果加了互斥锁的话,线程2必须等到开锁了之后条件满足了,才去唤醒线程1。
- 举一个比较容易懂的例子:一列人在排队买早饭:前面那个人买完了轮到下一个人了,(相当于唤醒下一个线程)但是下一个人突然去上厕所(条件不满足)。那么这个时候老板就.没有办法给下一个人卖饭(线程消失)。但是如果下一个人给给他前面的人说等他回来之后才可以离开(加了互斥锁),那么这个时候后面的那个人等待,等到他回来了(条件满足了),前面的人离开(唤醒下一个线程,)这时候就可以顺利的买饭啦。
使用条件变量的经典案例(生产消费模型)
#include"stdio.h"
#include"stdlib.h"
#include"string.h"
#include"unistd.h"
#include"pthread.h //线程所需要的头文件
#include"sched.h" //用互斥量所需要引进的头文件
pthrad_con_t cond; //条件变量的定义
pthread_mutex_t mutex; //互斥量的定义
#define CONSUMERS_COUNT 2
#define PRODUSERS_COUNT 2
struct msg
{
struct msg *next;//指向下一个要操作的
int num; //生产号
};
struct msg *head=NULL;
pthread_cond_t cond;//定义条件变量
pthread_mutex_t mutex;//定义互斥量
pthread_t threads[CONSUMERS_COUNT +PRODUSRS_COUNT];/
void *consumer(void *p)//消费者
{
int num =*(int *)p;
free(p);
struct msg *mp;
while(1)
{
pthread_mutex_lock(&mutex);//上锁,只允许一个线程进行消费
while( head==NULL)//没有可以消费的东西,可能是信号被打断了,唤醒信号还没有传递过来
{
pthread_cond_wait(&cond,&mutex);//等待其他线程的结束,条件满足
}
printf("d% begin consume\n",mp->num);//正常情况下,现场不阻塞,所以正常消费
mp=head;
head=mp->next;//消费完之后指向下一个要消费的
mp->next=mp;
pthread_mutex_unlock(&mutex);//解锁
printf("consume d%\n",mp->num);//刚刚消费线程结束,打印日志
free(mp);
printf("d% consume end\n",mp->num);
sleep(rand()%5);
}
}
void *producer(void *p)//生产者
{
struct msg *mp;//定义一个结构体指针mp
int num =*(int *)p;
free(p);
while(1)
{
printf("d% begin produce\n",num);//开始生产
mp=(struct msg*)malloc(sizeof(struct msg);
mp->num=rand()%1000+1;
printf("produce d%\n",mp->num);//打印要生产的生产号
pthread_mutex_lock(&mutex);//上锁,开始生产
mp->next=mp;//开始的时候指向头部
printf("d% product end",mp->num);
pthread_cond_signal(&cond);//发出信号唤醒正在等待的线程(消费线程)
pthread_mutex_unlock(&mutex);//先发出唤醒信号的原因是,如果唤醒了之后,消费线程没有准备好(条件不满足)那么可能会出现线程的消失
sleep(rand()%5);
}
int main(void)
{
srand(time(NULL));
pthread_cond_init(&cond,NULL);//条件变量的初始化
pthread_mutex_init(&mutex,NULL);//互斥量的初始化
int i;
for(i=0;i<CONSUMERS_COUNT;i++)
{
int *p=(int*) malloc(sizeof(int));
*p=i;
pthread_create(&thread[i],NULL,consumer,(void*)p);
}//创建消费者的线程
for(i=0;i<PRODUCERS_COUNT;i++)
{
int *p=(int*) malloc(sizeof(int));
*p=i;
pthread_create(&thread[CONSUMERS_COUNT+i],NULL,producer,(void*)p);
}//创建生产者的线程
for(i=0;i<CONSUMERS_COUNT+PRODUCERS_COUNT;i++)
{
pthread_join(&thread[i],NULL);
}//等待两个线程都结束
pthread_destroy(&mutex);
pthread_destroy(&cond);
线程的同步
- 虽然条件变量在进行等待时要配合互斥量使用,但是条件变量一般的时候是用于线程同步的。而还有一种线程之间的同步操作,可以达到无冲突的访问共享资源的目的,就是信号量。线程间的信号量和进程间的信号量作用相同。进程中比较经典的就是pv操作。
- 相关的函数可以参考以下这个博主的博客https://blog.csdn.net/ojshilu/article/details/23609701
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
#define _SIZE_ 64
int buf[_SIZE_];//仓库
sem_t full;//生产
sem_t empty;//消费,
//生产者
void* thread_producer(void* arg)
{
int i = 0;
while(1)
{
sleep(1);
sem_wait(&full);//生产者申请资源生产产品
int data = rand()%10000;//随机产生生产号
ringbuf[i] = data;
printf("Producer sell : %d\n",data);
i++;
i %=_SIZE_;
sem_post(&empty);//生产完之后,对资源进行释放,并且唤醒被阻塞的线程
}
}
//消费者
void* thread_consumer(void* arg)
{
int i = 0;
while(1)
{
sleep(1);
sem_wait(&empty); //消费者消费
printf("Consumer get : %d\n",ringbuf[i]);
i++;
i %= _SIZE_;
sem_post(&full);//每次消费一个,可供生产的资源也就会多一个
}
}
int main()
{
sem_init(&full,0,_SIZE_);
sem_init(&empty,0,0);
pthread_t producer;
pthread_t consumer;
pthread_create(&producer,NULL,thread_producer,NULL);
pthread_create(&consumer,NULL,thread_consumer,NULL);
pthread_join(producer,NULL);
pthread_join(consumer,NULL);
sem_destroy(&full);
sem_destroy(&empty);
return 0;
}
读写锁
- 由上图可知,读写锁的规则:读-读不互斥 读-写互斥 写-写互斥
- 写独占 读共享 写锁优先级高(只要是写锁,其他的请求都阻塞)
- 读写锁相较于互斥锁的优点仅仅是允许读锁的同时并发
- 以下是关于读写锁相关函数详解的一个链接:
- https://www.cnblogs.com/renxinyuan/p/3875659.html
乐观锁和悲观锁
并发控制
- 在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这个时候很容易产生冲突。
并发控制的机制分为两种:乐观锁和悲观锁。
-悲观锁:假设会发生并发冲突。传统的关系型数据库里边用到了很多这种锁机制,比如行锁,表锁等,都是在做操作之前先上锁。乐观锁:假设不会发生并发冲突,每次在拿数据之前不会认为被修改过。乐观锁适用于多读的应用类型,比如数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
如果在写比较少的情况下,适合用乐观锁不会那么容易发生冲突,省去了锁的开销。但是如果经常发生冲突,这时候用悲观锁才适合。
应用
乐观锁:使用自增长的整数表示数据版本号,更新时检查版本号是否一致。
- 悲观锁:需要使用数据库的锁机制。
- 一般时候乐观锁用的比较多一些。