线程安全:互斥量与条件变量

线程安全

线程安全:多个执行流对临界资源进行访问,但不会造成二义性
线程安全的实现:

同步:通过条件判断保证对临界资源访问的合理性
互斥:通过同一时间对临界资源的唯一访问实现临界资源访问的安全性

互斥的实现:互斥量
互斥锁实现互斥的原理:
互斥锁本身是一个只有0和1的的计数器,描述了一个临界资源当前的访问状态,所有执行流在访问临界资源之前都需要判断当前的临界资源是否可以被访问,
如果不允许则让执行流进行等待,如果允许就可以让执行流访问临界资源,但是在访问期间需要将临界资源修改为不可访问状态,这期间如果其他执行流想要访问,则不被允许。
互斥锁具体的操作流程以及接口的介绍:

1.定义互斥锁变量:pthread_mutex_t mutex;      
	 //pthread_mutex是一个结构体   typedef pthread_mutex pthread_mutex_t
2.初始化互斥锁变量:
	方式1. pthread_mutex_init(pthread_mutex_t* mutex,pthread_mutexattr_t* attr);
	方式2. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
		注意,使用方式2对互斥量进行初始化不需要销毁互斥量
3.在访问临界资源之前进行加锁操作(不能加锁则等待,可以加锁则修改资源状态,然后调用返回,访问临界资源)
	pthread_mutex_lock(pthread_mutex_t* mutex);
		阻塞加锁 --- 如果当前不能加锁(锁以及被别人加了),则一直等待知道枷锁成功调用返回。
	pthread_mutex_trylock(pthread_mutex_t* mutex);
		非阻塞加锁(尝试加锁) --- 如果当前不能加锁,则立即报错返回 --- EBUSY(这个变量忙,说明被加锁过了)
	挂起等待:将线程状态置为可中断休眠状态 -- 表示当前休眠;加入等待队列
	被唤醒:将线程状态置为可运行状态
4.在临界资源访问完毕之后进行解锁操作(将资源状态置为可访问,将其他执行流唤醒)  	
	pthread_mutex_unlock(pthread_mutex_t* mutex);
5.销毁互斥锁:
	pthread_mutex_destory(pthread_mutex_t* mutex);
注意操作事项:
	1.锁尽量只保护对临界资源的访问操作
	2.在任意有可能退出线程的地方退出前都要解锁

给为了保证所有执行流访问临界资源的安全,我们给所有执行流都进行了加锁解锁操作,那锁本事是不是一块临界资源呢?
所有的执行流都需要通过一个互斥锁实现互斥,意味着互斥锁本身就是一个临界资源,大家都会访问。
如果互斥锁本身的操作都不安全如何保证别人安全?
显然,互斥锁本身的操作是必须安全的,互斥锁进行的操作必须是原子操作的:
操作系统在访问互斥锁时,逻辑上,如果是将互斥锁上的数据加载到寄存器,然后再进行判断,可不可以访问,这个过程是需要几步完成的,在这个过程中,容易被其他线程操作打断,这样的访问不是原子的。
所以为了避免这种问题,实际上寄存器在访问时,直接与互斥锁的数据进行交换,一步完成。这样就可以一步完成,不会被其他线程打断。在这里插入图片描述
不管当前mutex是什么状态的,一步交换之后,其他线程都是不可访问的,这时候就可以慢慢判断了。
代码演示:

    1 /*===================================================================                              
    2  *
    3  *     这个demo以黄牛抢票为例来演示:
    4  *           如果多执行流访问临界资源不进行保护,可能造成的问题
    5  *
    6  *===================================================================*/
    7 #include<stdio.h>
    8 #include<unistd.h>
    9 #include<pthread.h>
   10 
   11  int ticket =100;
   12 pthread_mutex_t mutex;
   13 
W> 14 void* thr_scalpers(void* arg)
   15 {
   16   while(1)
   17   {
   18     //加锁一定是对你要保护的临界资源进行加锁,对一些不需要保护的也加锁就会降低程序的运行效率
   19     //加锁操作本来就会对程序造成很大程度的降低,但是为了保护多线程序安全,加锁也是没有办法的事
   20     pthread_mutex_lock(&mutex);
   21     if(ticket > 0){
   22       //有票就一直抢
   23       usleep(1000);
   24       printf("%d-----I got a ticket~~---%d\n",pthread_self(),ticket);
   25       ticket--;
   26       //完成一次临界资源访问之后,对临界资源解锁,让其他线程进行访问
   27       pthread_mutex_unlock(&mutex);
   28     }else{
   29       //加锁之后在任意一次可能退出线程的地方都要解锁
   30       pthread_mutex_unlock(&mutex);       
   31       pthread_exit(NULL);
   32     }
   33   }
   34   return NULL;
   35 }
   36 int main()
   37 {
   38   pthread_t tid[4];                                                                                
   39   int i,ret;
   40   //互斥锁的初始化一定要放在创建线程之前
   41   for(i=0;i<4;++i)
   42   {
   43     int ret = pthread_create(&tid[i],NULL,thr_scalpers,NULL);
   44     if(ret != 0)
   45     {
   46       printf("create thread failed~~\n");
   47       return -1;
   48     }
   49   }
   50   for(i=0;i<4;i++)
   51   {
   52     pthread_join(tid[i],NULL);
   53   }
   54   //互斥锁的销毁一定是不再使用这个互斥锁
   55   //pthread_mutex_destroy(&mutex);
   56   return 0;
   57 }                  

运行结果:在这里插入图片描述
我们可以看到,有四个线程在抢票,但实际上,所有的票都被同一个线程抢走了。
这就是互斥锁,可以保证安全性,但不可以保证合理性的特点

死锁:
死锁:多个执行流对锁资源进行争抢访问,但是因为访问推进顺序不当,造成互相等待最终导致程序无法正常推进,这就造成了死锁。
死锁实际上是一种程序无法推进,卡在某个位置。
死锁的产生通常是在访问多个锁的时候需要注意的情况。单一的锁是不会造成死锁的。
死锁产生的四个必要条件:

1.互斥条件:同一时间一个资源(一个锁)只能被一个进程访问。
	-- 同一时间,只有一个进程可以加减锁
2.不可剥夺条件:进程已获得的资源,在未使用完之前,不能被剥夺
	-- 我加的锁别人不能解,只有我自己使用完了自己解除,别人才能使用。
3.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
	-- 我加了A锁,去请求B锁,如果请求不到B锁,发生阻塞,也不会解除A锁。

4.环路等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
	-- 我加了A锁,然后去请求B锁;另一个人加了B锁,去请求A锁;这个时候,两者都等待。

这四个条件是死锁的必要条件,只要系统发生死锁,这四个条件必然成立,一项不满足,就不会造成死锁。
理解:

你和同桌吃饭,一人一根筷子(互斥条件),
拿到两双筷子才能吃,
你给他说你把你的给我(请求与保持),
否则别想要我的(不可剥夺条件)我吃完给你,
他也是这么想的,你和他的思想构成循环卡死。(环路等待条件)

死锁的预防:
破坏死锁的任意一个必要条件(1和2是锁本身具有的性质,主要避免3和4条件)

1.保持多方加解锁顺序一致。破坏 环路等待条件
2.非阻塞加锁,加锁失败则释放已有空间。破坏 请求与保持条件  pthread_mutex_trylock()
当锁加到一定数量之后,脑子已经不知道哪里会出现死锁了,就没法控制了。

死锁的避免:

死锁检验法;   ……
银行家算法; 源于银行贷款,有三个人来贷款,银行会评估贷款给谁我的资金链会出现断裂,如果贷给谁我的资金会进入非安全状态,就不给你。
	做法:将系统分为两种状态,安全-非安全
	思想:三张表
		所有资源表           已分配资源表           当前进程最大请求资源表
		Vailiable           Allocation       		   Need     
		当有线程请求锁资源时,会向系统发送请求的资源数,根据这三张表和发送的资源请求数判断,分给你我是否有可能造成环路等待,从而决定该不该将锁给你。

银行家算法的思路:
检查系统的安全/非安全状态;
一张表记录有哪些锁;一张表记录已经给谁分配了哪些锁;一张表记录当前需要哪些锁;
按照三张表进行判断,分配给谁指定的锁,会不会造成环路等待条件导致系统运行进入不安全状态,如果有可能,就不分配;反之,分配了之后不会造成环路等待,系统是安全的,则分配这个锁。 实际就是破环环路等待条件。

后续若不能分配锁,可以资源回溯,把当前执行流中已经加的锁释放掉。实际就是破坏请求与保持。
非阻塞加锁操作,若不能加锁,则把手上的其他锁也释放掉。实际上就是破环请求与等待。

复习思路:
死锁如何产生,如何预防,如何避免
拓展问题:
枷锁对临界资源进行保护,实际上对程序的性能是一种加大的挑战;
在高性能程序中通常会讲究一种无锁编程 、CAS 锁/一对一的阻塞队列、atomic原子操作、乐观锁、悲观锁、读写锁、自旋锁

同步的实现:

我们知道,同步是通过条件判断来保证临界资源访问的合理性。故而同步的实现,就是依靠条件变量的。
线程在满足资源的访问条件时才能去访问,否则就挂起线程;直到满足条件之后再去唤醒线程。
条件变量: 向外提供了一个使线程等待 的接口和一个唤醒线程的接口,还有一个pcb等待队列。
因为条件变量只提供了使线程等待和唤醒线程的接口,因此无论什么时候让线程等待和唤醒线程都是需要程序员在进程中判断的(访问条件是否满足的判断由我们自己来决定)。
上面说到锁是一块临界资源,那么条件变量也是一样的,但锁这块临界资源在获取的时候CPU寄存器和内存上的资源交换一步完成,保证了操作是原子性的,但条件变量可就没这个特性了,它和普通临界资源一样,CPU将条件变量的数据加载到寄存器,然后再进行判断,可不可以访问,这个过程是非原子的。
所以我们在使用条件变量时,为了保护临界资源/线程安全,需要在使用前加锁保护,使用后解锁。
操作步骤:

1.定义条件变量  ---  pthread_cond_t 
2.初始化条件变量 
	    pthread_cond_init(pthread_cond_t* ,pthread_cond_condattr_t); 
		pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
3.使线程挂起休眠的接口:
		这个接口完成解锁、休眠(阻塞)、加锁三个步骤,这并不是一步完成的,我们并不能保证它的原子性,而wait这个接口就是保证了解锁和休眠的原子性。
			如果不保证原子性,线程1解锁之后,还没休眠就时间片切换执行线程2,
		线程2进来可以加锁,执行完成之后唤醒线程1,线程1开始执行,这时刚进来的一步就是陷入休眠。
		之后可不会有线程唤醒它了,程序就卡在这里了。所以必须保证解锁和休眠是原子操作
		pthread_cond_wait(pthread_cond_t* ,pthread_mutex_t* mutex);
			-- 一直死等
		pthread_cond_timewait(pthread_cond_t* ,pthread_mutex_t* ,struct timespec);
			-- 最后一个参数有所不同,是一个时间结构(/usr/include/time.h),秒和纳秒,如果时间到了没有被唤醒,就自动唤醒。
	注:条件变量需要搭配互斥锁一起使用(判断是否满足的条件,本身是一个临界资源,需要被保护,所以搭配互斥锁保护)
4.唤醒线程的接口:
	    pthread_cond_signal(pthread_cond_t* ) ---  唤醒至少一个线程(并不是单个)
		pthread_cond_broadcast(pthread_cond_t* ) --- 唤醒所有正在等待的线程
5.销毁条件变量:pthread_cond_destory(pthread_cond_t* );

代码展示:

    1 /*==================================================================                               
    2  *
    3  *         这个demo演示条件变量的基本使用流程与注意事项
    4  *
    5  *==================================================================*/
    6 
    7 #include<stdio.h>
    8 #include<unistd.h>
    9 #include<stdlib.h>
   10 #include<pthread.h>
   11 
   12 int bowl = 0;        //默认0为碗中没有饭
   13 
   14 pthread_cond_t cook_cond;     //实现线程之间对bowl变量访问的同步操作
   15 pthread_cond_t customer_cond;     //实现线程之间对bowl变量访问的同步操作
   16 pthread_mutex_t mutex;   // 保护bowl变量的访问操作
   17 
W> 18 void* thr_cook(void* arg)
   19 {
   20   while(1){
   21     //注意这个判断条件必须是while循环判断,否则多个顾客线程和多个厨师线程就会出现争抢着加锁的状态导致程序不安全
   22     while(bowl != 0){   //表示有饭,不满足做饭条件
   23       //让厨师先等待,等待之前先解锁,唤醒之后再加锁
   24       //pthread_cond_wait()
   25       //这个接口中,就已经实现了解锁,休眠,等待这三部操作
   26       //并且解锁和休眠是一步完成,保证原子操作
   27       pthread_cond_wait(&cook_cond,&mutex);
   28     }
   29     bowl = 1;  //能走下来表示没饭,bowl==0,这个时候做一碗饭,将bowl置为1,表示有饭了
   30     printf("I made a bowl of rice~~\n");
   31     //唤醒顾客吃饭
   32     pthread_cond_signal(&customer_cond);
   33     //解锁
   34     pthread_mutex_unlock(&mutex);
   35   }
   36   return NULL;                                                                                     
   37 }
   38 
W> 39 void* thr_customer(void* arg)
   40 {
   41   while(1)
   42   {
   43     //加锁
   44     pthread_mutex_lock(&mutex);
   45     //注意这个判断条件必须是while循环判断,否则多个顾客线程和多个厨师线程就会出现争抢着加锁的状态导      致程序不安全
   46     while(bowl != 1){   //没有饭,先等待
   47       //没有饭则等待,等待前先解锁,被唤醒后加锁
   48       pthread_cond_wait(&customer_cond,&mutex);
   49     }
   50     bowl = 0;     //能走下来说明有饭,吃完就没有了,把bowl置为0;
   51     printf("I had a bow of rice~~It is delicious!\n");
   52     //唤醒厨师做饭
   53     pthread_cond_signal(&cook_cond);
   54     //解锁
   55     pthread_mutex_unlock(&mutex);
   56   }
   57   return NULL;                                                                                     
   58 }
   59 
   60 
   61 int main()
   62 {
   63   pthread_t cook_tid[4],customer_tid[4];
   64   int ret,i;
   65   //初始化上面定义的两个变量,互斥锁和条件变量
   66   //初始化必须放在创建线程之前
   67   pthread_mutex_init(&mutex,NULL);
   68   pthread_cond_init(&cook_cond,NULL);
   69   pthread_cond_init(&customer_cond,NULL);
   70 
   71   for(i=0;i<4;i++){
   72     ret = pthread_create(&cook_tid[i],NULL,thr_cook,NULL);
   73     if(ret != 0 )
   74     {
   75       printf("create thread error~~\n");                                                           
   76       return -1;
   77     }
   78     ret = pthread_create(&customer_tid[i],NULL,thr_customer,NULL);
   79     if(ret != 0 )
   80     {
   81       printf("create thread error~~\n");
   82       return -1;
   83     }
   84   }
   85   pthread_join(cook_tid[0],NULL);
   86   pthread_join(customer_tid[0],NULL);
   87   pthread_mutex_destroy(&mutex);
   88   pthread_cond_destroy(&cook_cond);
   89   pthread_cond_destroy(&customer_cond);
   90   return 0;
   91 }           

在这个过程中我们发现了几个问题:
问题一:
在单一厨师线程和单一顾客线程展示时,我们的程序运行完好,总是厨师做一碗饭,顾客吃一碗饭;
但在多个厨师线程和多个顾客线程展示时,我们发现,会出现厨师做了一碗饭,顾客吃了好几碗;或者顾客吃了一碗,厨师做了好几碗这样的逻辑错误:在这里插入图片描述
仔细分析这个过程:
程序刚开始(这个时候还没有饭),多个顾客线程,第一个顾客线程开始运行,加锁,没有饭,解锁阻塞,然后第二个顾客线程检测到没有锁,也进来加锁,没有饭,解锁阻塞;第三个也是。(这三个线程都被卡在阻塞这一步)
厨师线程这边,进来加锁检测到没有饭做饭,然后去唤醒顾客线程自己解锁阻塞。
由于唤醒线程这个接口,会至少唤醒一个,假设一下唤醒了三个顾客线程,都不阻塞了。三个线程就会开始抢着加锁,有一个顾客线程抢到锁加上之后去吃饭,其他两个酒杯卡在加锁这里。吃饭的那个顾客线程吃完之后解锁唤醒厨师做饭。
问题是,这个时候,厨师被唤醒了要加锁,但是上面两个顾客线程也要加锁,谁抢到算谁的(时间片切换),如果是顾客抢到锁加锁成功,就会在没有饭的情况下直接去吃了一碗饭。(这是逻辑错误的
同理,也会出现厨师线程在有饭碗的情况下做了一碗饭。
避免方法:
条件的判断应该是一个循环的判断,顾客线程被唤醒成功加锁之后,重新判断有没有饭,没有就休眠,有就吃饭。

问题二:
在我们将if条件判断更改为while循环判断时,又出现问题了:
在这里插入图片描述
我们发现程序逻辑正确了,但是程序运行到一般,卡住了。分析原因:
条件变量(两个接口+pcb等待队列)只有一个,也就是pcb等待队列只有一个,
顾客没有吃到饭,就要挂到等待队列上,厨师不能做饭也要挂到等待队列上,
三个厨师线程,在有饭的情况下,就会挂到等待队列上
假设一个顾客线程吃完饭,要唤醒厨师,唤醒一个厨师后,因为没有饭,重新挂载到等待队列上;
被唤醒的厨师,做了一碗饭后,要唤醒挂载在等待队列上的顾客线程,但是因为这个等待队列上既有厨师又有顾客,
可能没有唤醒顾客而唤醒了厨师,被唤醒的厨师发现有饭,因为之前的while循环判断,厨师又会重新判断,不满足做饭的条件,就重新陷入休眠 状态,导致程序阻塞;
所以总结成一句话就是,问题出在,一个条件变量,只有一个等待队列。
解决方案:
不同角色的线程应该挂载在不同的等待队列(不同的条件变量)上进行等待,唤醒的时候就可以有目的的进行唤醒,就不会出现这种情况。
所以我们再分别为cook和customer加上条件按变量,分别进行条件判断和唤醒,就可以解决问题了。
这就是同步,可以保证合理性但很难保证安全性的特点;

同步总结/条件变量使用规则

使用条件变量的注意事项:
	1.条件变量需要搭配互斥锁一起使用,pthread_cond_wait集合了解锁/休眠/被唤醒后加锁的三步操作。
	2.程序员在程序中对访问条件是否满足的判断需要使用while循环进行判断。
	3.在同步实现中,多种不同的线程角色应该使用不同的条件变量,不要让所有的线程都等待在同一个条件变量上。

生产者与消费者模型

生产者与消费者模型:大佬们针对典型的应用场景设计的解决方案
生产者与消费者模型应用场景:有线程不断地产生数据,有线程不断地处理数据。
数据的产生与数据的处理,如果放在同一个线程中完成,因为执行流只有一个,那么肯定是生产一个处理一个,处理完一个后才能生成一个。
这样的依赖关系太强了,如果处理的比较慢,就会把程序的速度拖慢下来。
因此将生产与处理放在不同的执行流中完成,那么生产和处理的线程如何实现通信呢?
中间增加一个数据缓冲区(队列),作为中间的数据缓冲场所。
产生线程只负责将数据放入缓冲区,处理数据只负责将数据从缓冲区中取出并处理。
在这里插入图片描述
生产者与消费者模型解决的问题:

1.解耦合 
	将模块分开,耦合度降低两边都会比较灵活,各自可以按照各自的业务压力创建线程处理,
	且处理模块代码发生改变只需要改变处理模块代码。
2.支持忙闲不均 
	缓冲区大小可以一直申请,是可调的,数据多了就可以放在缓冲区。
3.支持并发        可以有多个执行流进行处理

通过互斥锁与条件变量实现一个消费者模型/线程安全的队列:

#include<iostream>
#include<stdio.h>
#include<queue>
#include<pthread.h>

#define MAXCAP 3

class BlockQueue
{
  private:
    std::queue<int> q;
    size_t cap;
    pthread_mutex_t mutex;
    pthread_cond_t Cus_cond;
    pthread_cond_t Pro_cond;
  public:
    BlockQueue(size_t c = MAXCAP):cap(c)
  {
    //mutex=PTHREAD_MUTEX_INITIALIZER;      //C++不用这个
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&Cus_cond,NULL);
    pthread_cond_init(&Pro_cond,NULL);
  }
    ~BlockQueue()
    {
      pthread_mutex_destroy(&mutex);
      pthread_cond_destroy(&Cus_cond);
      pthread_cond_destroy(&Pro_cond);
    }
    bool push(const int& data)
    {
      //要往缓冲区队列放数据,就得先判断缓冲区是否满了,所有线程都可以访问缓冲区,那么缓冲区就是临界资源
      //需要进行保护
      pthread_mutex_lock(&mutex);
      //循环条件防止时间片调度产生的访问出错
      while(q.size() == cap)
      {
        pthread_cond_wait(&Pro_cond,&mutex);//解锁阻塞加锁
      }
      q.push(data);
      pthread_cond_signal(&Cus_cond);
      pthread_mutex_unlock(&mutex);
      return true;
    }
    bool pop(int* data)
    {
      pthread_mutex_lock(&mutex);
      while(q.empty() == true)
      {
        pthread_cond_wait(&Cus_cond,&mutex);
      }
      *data = q.front();
      q.pop();
      pthread_cond_signal(&Pro_cond);
      pthread_mutex_unlock(&mutex);
      return true;
    }
};


void* producter(void* arg)
{
  int data = 0;
  BlockQueue* q = (BlockQueue*)arg;
  while(1)
  {
    q->push(data);
    printf("put data %d\n",data++);  //注意插入和打印这块不是原子操作,如果线程多了,我们通过打印看到的结果有可能是不对的,但是其实际上只要逻辑闭环,就没有问题
    //后++,插入之后数据变化一下
  }
  return NULL;
}

void* customer(void* arg)
{
  BlockQueue* q = (BlockQueue*)arg;
  while(1)
  {
    int data =0;  //由于我们接受数据这里,消费者拿到数据处理,这里的data是一个输出型参数,初不初始化一样
    q->pop(&data);
    printf("get data: %d\n",data--);
  }
  return NULL;
}


int main()
{
  pthread_t ptid[3],ctid[3];
  BlockQueue q;
  int ret;
  for(int i =0;i<3;++i)
  {
    ret = pthread_create(&ptid[i],NULL,producter,&q);
    if(ret != 0)
    {
      printf("create producter thread %d failed!\n",i);
      return -1;
    }
    ret = pthread_create(&ctid[i],NULL,customer,&q);
    if(ret != 0)
    {
      printf("create customer thread %d failed!\n",i);
      return -1;
    }
  }

  for(int i=0;i<3;++i)
  {
    pthread_join(ptid[i],NULL);
    pthread_join(ctid[i],NULL);
  }

  return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值