线程安全--互斥锁实现,互斥锁相关知识,死锁

**

线程安全–互斥锁实现

线程安全的概念
多个线程对临界资源的合理性访问。
临界资源:多个执行流 共享的资源叫做临界资源
临界区:每个线程内部,处理临界资源的代码,就叫做临界区。
如何实现线程安全:同步(不保证安全)与互斥(不保证合理)
同步实现:使多个线程在某种规则条件下,实现在资源共同访问的合理性。使用条件变量和信号量实现
互斥实现:在某一个时间,只有一个线程可以访问资源,实现访问的安全性。使用互斥许锁和信号量实现

互斥实现线程安全的原理
在这里插入图片描述
黄牛抢票的例子
有100张票,需要被抢,他就是临界资源。创建4个线程一块去抢,相当于多线程。这个时候,可能出现一个黄牛抢了所有的票,并且在票为负时,黄牛还能抢票。
为什么出现这种结果
1、在我们判断临界资源还有的时候,这个时候代码并发切换到其他线程。其他线程刚好访问了最后的资源,然后切换至这个线程,导致没有资源,但是这个线程还访问了。
2、对于资源的–可能不是一个原子操作,中间会被打断。(原子操作:不会被任何的调度机制打断,要么全部完成,要么不完成)

**所以为了实现线程的安全,可以考虑一个互斥锁mutex,根据上图的原理,在执行临界区时先加上锁,离开临界区时解锁
如何加锁
1、代码要有互斥能力,当有一个线程执行到临界区,则阻止其他线程进入临界区。
2、当没有线程在临界区,多个线程要求同时执行临界区,只能一个线程进入临界区。
3、如果某线程不在临界区,是不能够阻止其他线程进临界区。

互斥锁:互斥锁本身就是一个0/1的计数器,它用来标志着临界资源是否可以被访问的状态。当线程要去访问临界资源的时候,必须先去访问互斥锁,看是否可以访问临界资源。
可以访问将访问状态置为不可访问,再进入临界区进行资源访问,当访问完临界资源之后,一定要将临界资源的状态转换为可访问状态;
不可以访问,则阻塞或者报错。

关于互斥锁mutex所有函数操作
1、定义互斥锁变量
pthread_mutex_t mutex;
2、初始化互斥锁变量
互斥锁静态分配–不需要销毁
pthread_mutex_t mutex = PTHREAD_ MUTEX_ INITIALIZER ;
互斥锁动态分配–需要销毁
pthread_mutex_init(pthread_mutex_t *mutex,pthread_mutex_t *attr)
pthread_mutex_init(&mutex,NULL); 创建线程之前对锁进行初始化
3、访问临界资源(执行临界区)之前进行加锁操作。因为不知道临界资源现在的访问状态是什么,所以加锁是一个尝试的过程。
pthread_mutex_lock(pthread_mutex_t *mutex) --加不上锁阻塞等待,然后等着可访问临界资源状态的时候。
pthread_mutex_trylock(pthread_mutex_t *mutex) --加不上锁则报错返回EBUSY。;
pthread_mutex_lock(&mutex);–加锁一定是访问临界资源之前一步
4、临界资源访问完成后进行解锁操作(将临界资源的访问状态定义为可访问状态,唤醒其他线程)
pthread_mutex_unlock(pthread_mutex_t * mutex);
pthread_mutex_unlock(&mutex);**在任意离开临界资源访问的位置都要进行解锁。


5、销毁互斥锁,不要销毁已经加锁的互斥锁
pthread_mutex_destroy(pthread_mutex_t
*mutex)
pthread_mutex_destroy(&mutex);–互斥锁的销毁一定是不在使用这个互斥锁

#include <stdio.h>                                 
#include<unistd.h>
#include<pthread.h>
int ticket = 100;//;临界资源
pthread_mutex_t mutex;//定义一个锁,为了保护临界资源
 void* thr_scalpers(void* arg)
 {
  while(1)
  {       
  //加锁一定是只保护临界资源的访问
  pthread_mutex_lock(&mutex);
     if(ticket > 0)
    {
      usleep(1000);
      printf("i got a ticket~~%d\n",ticket);
      ticket--;
      pthread_mutex_unlock(&mutex);
    }
     else
    {
      pthread_mutex_unlock(&mutex);//加锁之后在任意的线程退出的地方都需要去解锁
      pthread_exit(NULL);
    }
  }
}

 int main()                            
 {                               
    pthread_t tid[4];                       
    int i = 0;                                  
    pthread_mutex_init(&mutex,NULL); //在线程创建之前对互斥锁进行初始化 
  for(i = 0;i < 4;i++)                      
  {     
     int ret = pthread_create(&tid[i],NULL,thr_scalpers,NULL);
    if(ret != 0)
   {
      printf("creat pthread failed\n");
      return -1;
   }
 }
 for(i = 0;i < 4;i++)
 {
     pthread_join(tid[i],NULL); //因为4个线程需要不停的去抢占资源,所以他们最好是进行线程等待。如果不线程等待,那么就运行到主线程的return,就会进程退出。
     //默认情况下的线程,是joinable属性,如果不进行线程等待,可能会浪费资源。
 }
     pthread_mutex_destroy(&mutex);//互斥锁的销毁一定是不在使用这个互斥锁
     return 0;
 }

关于互斥锁一个问题:所有的执行流都通过一个互斥锁来实现线程安全,意味着互斥锁本身就是一个临界资源。如何保证互斥锁的访问是安全的?–看互斥锁是不是原子操作
背景: 冯诺依曼体系结构, cpu处理任何指令都是在寄存器上进行的,处理完毕后在加载到内存中。

正常情况下,要
1、将mutex内存的值加载到寄存器中
2、判断寄存器中的值是否为1
if(== 1)
加锁,
然后将0从寄存器写到mutex内存中。–主要是将0在判断之前就给了mutex的内存,这样就能让寄存器慢慢判断了。
再然后访问资源。
else
线程等待
这个过程在汇编需要很多步才能完成,cpu又是不停的切换线程调度,所以互斥锁并不是原子操作。

保证互斥锁原子操作:
1、将寄存器先置为0
2、交换寄存器与mutex的值。–这样mutex内存值为0,切换到其他线程也是不能被访问的。寄存器慢慢判断
if(== 1)这个时候表示刚刚寄存器交换过来的值是1,那就执行下边步骤
加锁,
访问资源
else 这个时候表示刚刚与寄存器换的那个mutex的值是0,所以要考虑先解锁。
线程等待

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

**

死锁

**
加锁对临界资源保护,实际对程序的性能提出了一个较大的挑战。–死锁
高性能程序一般会讲究一种无锁编程。–CAS锁 一对一阻塞队列/atomic原子操作

死锁概念:描述的是程序流程无法向前推进的情况。多个线程对锁资源进行争取,因为推进访问的顺序不同,造成互相等待。
死锁什么时候产生
单个锁,加锁忘了解锁会产生死锁。
多个锁,一不留神就会产生死锁。
死锁产生的必要条件
1、互斥条件:同一个时间只有一个线程可以操作互斥锁。
2、不可剥夺条件:如果某个线程加了锁,其他线程是不能够解开的。
3、请求与保持条件:如果一个线程加了A锁,请求去加B锁,不能对B加锁,则也不会释放A。
4、环路等待条件:一个线程加锁A,请求加B,另一个线程加锁B,请求加A,则会环路等待。
死锁产生并不一定是这四个条件导致的。但是这四个条件一定会产生死锁。

死锁的预防
破坏死锁产生的必要条件 3 4 。
统一加解锁顺序。
避免锁未释放场景
资源一次性分配

死锁的避免
1、死锁检测算法
2、银行家算法
三张表:一张表记录当前有哪些锁,二张表记录已经给谁分配了哪些锁,三张表记录谁当前需要哪些锁。

给一个执行流分配指定的锁,会不会造成环路等待条件,如果会,则不分配这个锁给这个执行流。
若后续不能分配锁了,则资源回溯,将当前加的锁都释放掉。–破坏请求与保持条件。
如果不是因为阻塞而不能加锁的任一种情况,则应该将当前的锁也释放掉–破坏请求与保持条件。

调研锁的种类
乐观锁:CAS锁就是乐观锁。
悲观所:互斥锁就是一种悲观锁。
可重入锁:一个线程可以对一个锁加多次。
不可重入锁:一个线程可以对一个锁加一次。
读写锁:对应读者写者模型–读共享写互斥
读共享,加读锁,前提是没人加写锁
写互斥,加写锁,前提是没人加读锁和写锁。
自旋锁:不放弃cpu,一直对条件进行循环判断
在这里插入图片描述


版权声明:本文为weixin_44229957原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。