Linux之线程安全

目录

一、线程安全问题引入

预热代码

背景知识

改动下代码

线程不安全的产生

二、互斥

2.1互斥概念

2.2.互斥锁

问题一.防君子不防小人

问题二.两个线程可能同时加锁成功

2.3 互斥锁的计数器当中如何保证原子性

2.4互斥锁的接口

1.初始化互斥锁的接口

1.1动态初始化:

1.2静态初始化(直接用宏来初始化互斥锁):

2.加锁接口

3.解锁接口

4.销毁接口

代码展示

 三、同步

1.有了互斥还为什么要有同步?

条件变量

1.条件变量的使用原理

2.条件变量的原理

3.条件变量接口

3.1初始化接口:

3.2等待接口:

3.3唤醒接口:

四、代码展示

五、条件变量夺命追问

1.条件变量的等待接口第二个参数为什么会有互斥锁?

2.pthread_cond_wait的内部是针对互斥锁做了上什么操作?先释放互斥锁还是先将线程放入到PCB等待队列?

 3.线程被唤醒之后会执行什么代码?需要再获取互斥锁吗?


一、线程安全问题引入

预热代码

我们在了解线程安全之前先来写一个代码

模拟黄牛进行抢票,创建两个线程,模拟两个黄牛的角色

我们来写一下代码

 我们来运行一下:

 我们发现似乎和我们预想的不大一样,本以为是两个黄牛互相抢票,但现在显示的结果却是一个黄牛拿到了全部的票

我们可以来分析一下这个结果产生的原因

 经过分析可以知道,产生这种结果是正常的现象,并不是线程安全问题

真正有问题的情况是线程A,B同时抢到了同一张票,或者抢到了不合法的票(负数)

背景知识

 我们再来思考一下:

我们之前说了,多线程存在就是为了提高程序的运行效率的,但上面的结果是一个线程就把所有的活干完了,

这怎么能代表提高效率呢?

我们这时候就要开始思考到底要怎样解决问题了

改动下代码

 我们在每次抢票的时候,让黄牛先等一会,这样就能保证另一个黄牛有机会能够抢到票

我们来运行一下

 可以发现,这时候就是两个黄牛来进行交替抢票了

但sleep(1),代码sleep的时间太长了,我们可以使用usleep(),这是以微秒结的

线程不安全的产生

 再去运行一下:

 这时候就出现问题了,我们看到两个黄牛同时抢到了100这个票,这明显就是不安全的

这是什么原因导致程序二义性的产生呢?

我们来详细分析一下这个代码:

 二义性产生的原因:两个线程看似减了两次,但结果其实只是减了一次

上面这是并发(一共cpu执行多个进程),都已经发生了线程不安全问题,

       那么并行(多个cpu执行多个进程)可想而知就更加不安全了

那么怎么解决不安全的问题呢?

我们刚刚多线程不安全产生的原因就是多个线程能够同时访问g_tickets

为了解决这个问题,我们可以加个保护,让一次只有一个线程能访问g_tickets,那么如何做到保护呢?

二、互斥

  • 2.1互斥概念

  • 互斥的要做的事情:控制线程的访问时序。 当多 个线程能够同时访问到临界资源的时候,有 可能会导致线程执行的结果产生二义性。而互斥就是要保证多个线程在访问同一个临界资源执行临界区代码的时候 (非原子性性操作(线程可以被打断) ),控制访问时序。让一个线程独占临界资源执行完,再让另外一个独占执行;

临界资源:能被多个线程同时访问到的资源

临界区代码:访问临界资源的代码

  • 2.2.互斥锁

  • 互斥锁的原理

  • 互斥 锁的本质就是0/1计数器,计 数器的取值只能为0或者1
    • 计数器的值为1 :表示当 前线程可以获取到互斥锁,从而去 访问临界资源
    • 计数器的值为0 :表示当前线程不可以获取到互斥锁,从而不能访问临界资源

加锁:加锁成功的时候,会将计数器的值从1 改为 0;

解锁:解锁成功的时候,会将计数器的值从0 改为 1;

问题一.防君子不防小人

 但这里要注意的是:这有可能是防君子不防小人的,例如:线程A遵守互斥锁的规则,每次都先拿锁,但有可能线程B会不管这个锁,直接区访问临界区的资源,所以我们在写代码的时候要注意这个情况

问题二.两个线程可能同时加锁成功

 为了使计数器本身就是互斥的(只有一个线程能加锁),这就需要互斥锁是原子性的

2.3 互斥锁的计数器当中如何保证原子性

  • 1.加锁的时候:寄存器当中的值设置为(0)
  • 第一种情况:计数器的值为1,说明锁空闲, 没有被线程加锁

  • 第二种情况:计数器的值为0,说明锁忙碌,被其他线程加锁拿走(此时当前线程进入等待状态:等待其他线程访问完毕将锁打开)

 2.解锁的时候,只需要将寄存器中的值预设为1就行了

  • 2.4互斥锁的接口

  • 1.初始化互斥锁的接口

  • 1.1动态初始化:

int pthread_mutex_init(  pthread_mutex_t *restrict  mutex, const pthread_mutexattr_t *restrict  attr);

  • 参数
  • pthread_mutex_t 互斥锁类型(结构体)
  • mutex互斥锁指针:可以从堆上开辟一块空间传进去也可以定义一个对象
  • attr:互斥锁的属性(一般传递为NULL采用它的默认属性)

1.2静态初始化(直接用宏来初始化互斥锁):

  • pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;(是一个宏)

2.加锁接口

  • 2.1int pthread_ mutex_ lock(pthread_ mutex_ t *mutex)(互斥锁的阻塞加锁接口,拿不到锁就阻塞等待,直到拿到锁) ;
  • 2.2int pthread. mutex_ trylock(pthread_ mutex_ t *mutex);非阻塞加锁接口,拿锁的时候如果拿不到锁就直接返回了,也不等待,所以这个函数需要通过返回值判断是否加锁成功,需要搭配循环使用)
  • 2.3int pthread_ mutex_ _timedlock(pthread_ mutex_ t *restrict mutex,const struct timespec *restrict abs_ t imeout);这是一个带有超时时间的加锁接口

  • 3.解锁接口

  • int pthread_ mutex_ unlock(pthread_ mutex_ t *mutex);

  • 4.销毁接口

  • int pthread_mutex_destroy(pthread. _mutex_ _t *mutex);
  • 如果是动态初始化互斥锁,就需要销毁,如果是静态初始化就不用销毁。

代码展示

我们先来写一个代码,这是对之前的黄牛抢票代码的更改

我们运行一下:

 发现只有一个线程抢了所有的票,而且最后程序还没有退出,这是为什么呢?

我们来分析一下

我们线通过pstack来看看这个进程在干什么事情

我们刚刚写的代码是只有一个线程能够拿到锁,另一个是怎么也拿不到锁的

 

 我们要对代码进行修改

这时候还保证了临界区代码的原子性,只有一个线程能成功加锁并操作

 这时候再去运行一下

这时候就能正常看到两个线程交替拿票了,且不会出现错误

 其中这个里有个结论:在线程所有有可能退出的地方都进行解锁,不要让线程退出的时候把锁带走

 三、同步

1.有了互斥还为什么要有同步?

多个线程保证了互斥, 也就是保证了线程能够合理的访问临界资源了。但并不是说, 各个线程在访问临界资源的时候都是合理的。同步是为了保证多个线程对临界资源的访问的合理性。这个合理性建立在多个线程保证互斥的情况下

如果只保证互斥,有可能出现“线程饥饿”问题,那么多线程不久变成了单线程吗?

比如现实生活种的问题,去面馆吃面

我们在吃面的生活,不可能我们边吃碗里的面,厨师边往碗里做面,防止这样的事就是互斥的原理,但是还有一个问题,就是厨师不能疯狂的一直往碗里方面,碗是盛不下的,或者我们一直吃碗里的面,甚至把碗给吃了。这就是同步要去解决的问题

所以同步就是:在保证互斥的前提下,保证多个线程对临界资源访问的合理性

条件变量

  • 线程在加锁之后,判断下临界资源是否可用(这里就像拿到碗之后,可不可以做面,就是当前碗里有没有面,如果没有面,那就可以做面,如果有面就不可以做面,将互斥锁放开然后进行等待,吃面也同理):
  • 如果可用:则直接访问临界资源
  • 如果不可用:则调用等待接口,让该线程进行等待

1.条件变量的使用原理

  • 线程在加锁之后,判断下临界资源是否可用(这里就像拿到碗之后,可不可以做面,就是当前碗里有没有面,如果没有面,那就可以做面,如果有面就不可以做面,将互斥锁放开然后进行等待,吃面也同理):
  • 如果可用:则直接访问临界资源
  • 如果不可用:则调用等待接口,让该线程进行等待
  • 2.条件变量的原理

  • 本质上是:,PCB等待队列 (存放在等待的线程的PCB)

  • 3.条件变量接口

  • 3.1初始化接口:

    • 1.动态初始化

 int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

  • 参数:
  • pthread_cond_t:
    • 条件变量类型
  • cond:
    • 接收一个条件变量的指针||接收一个条件变量的地址
  • attr:
    • 表示条件变量的属性信息,传递NULL,采用默认属性
  • 2.销毁条件变量:
    • int pthread_cond_destroy(pthread_cond_t *cond);
  • 3.销毁条件变量:
    • int pthread_cond_destroy(pthread_cond_t *cond);

  • 3.2等待接口

int pthread_cond_wait(pthread_cond_t    *restrict cond,pthread_mutex_t     *restrict mutex);

作用:谁调用把谁放在PCB等待队列当中

  • 参数:
  • cond:
    • 条件变量
  • mutex:
    • 互斥锁
  • 3.3唤醒接口:

  • 作用:通知PCB等待队列,唤醒在队列中的线程
  • int pthread_cond_broadcast (pthread_cond_t *cond);//唤醒PCB等待队列中所有的线程
  • int pthread_cond_signal (pthread_cond_t *cond);//唤醒PCB等待队列当中至少一个线程(有可能唤醒两个或者三个或者全部都唤醒)

四、代码展示

这个代码的效果是一会为正一会为负数,这显然是没有对碗这个资源进行合理的使用的

我们再写一个代码,在不用到条件变量的情况下,用if语句来进行

 我们运行一下来观察结果

 我们可以看到这样确实可以做到吃面和做面是正确着的,那既然这样就可以了为什么还要用到条件变量呢?

缺陷:CPU的利用率很低

假设一个线程拿到的时间片是200ms,在这个时间片范围内,他会先判断,面人如果当前碗里没有面那么他就continue退出这一次循环,但是这时候还没200ms,所以他就会一直在这里重复这个判断和continue,也可能他在刚加完锁时间片就到了,这时候他退出了,然后做面人来了,但是因为锁是加着的,所以做面人也访问不到碗,那么在这个时间片范围内就是在做无用功,时间白白给浪费掉了。

 我们这时候加上条件变量再看看看

我们这时候来看看调用堆栈,看看是不是和我们分析的是一样的

我们可以看到,吃面人是在pthread_cond_wait中,做面人是在sleep中。

但是上面代码中写了两个sleep,而且按我们之前的分析,做面人是加不了锁的,那怎么能走到下面的sleep呢?

为了区分到底是在上面还是下面,我们在两个sleep中间打印一句话,如果能打印出来就是下面的sleep中

我们发现了这句话,证明做面线程是在下面的sleep中 ,而且做面人加锁成功了

我们这时候就要知道在pthread_cond_wait中,会进行解锁逻辑

我们之前的现在写的代码已经能够做到吃一碗面做一碗面了,但如果要是有多个吃面人和做面人会怎样呢?

 我们这时候在来运行一下看看会怎么样

 我们来分析一下为什么会产生这种错误的现象

 那这个代码应该怎么改去解决问题呢?

之前那个判断逻辑有问题,有可能做面人被唤醒之后资源还是不可用的,所以要再次进行判断,将if改为while

 这时候我们再来运行一下,观察现象

 这时候确实是做一碗面吃一碗面,但现在这个程序不结束了,而且也不再进行吃面或者做面了

 我们这时候再来看一下这几个线程在干什么呢?

 我们神奇的发现,这四个线程都在pthread_cond_wait

我们来分析一下这个现象又是如何产生的

 那么怎么解决这个问题呢?

我们可以多加一个条件变量

 我们来写代码实现这个逻辑

 这时候我们再来运行一下:

 我们发现,这时候已经能够顺利的实现功能了,这才算真正支持了多线程

五、条件变量夺命追问

1.条件变量的等待接口第二个参数为什么会有互斥锁?

在函数内部需要解锁操作,如果不解锁就阻塞等待,则其他线程一定拿不到锁

2.pthread_cond_wait的内部是针对互斥锁做了上什么操作?先释放互斥锁还是先将线程放入到PCB等待队列?

解锁,   先放到PCB等待队列再进行解锁

 3.线程被唤醒之后会执行什么代码?需要再获取互斥锁吗?

1.从PCB等待队列当中移除

2.抢锁

猜你喜欢

转载自blog.csdn.net/flyingcloud6/article/details/127854474
今日推荐