线程安全之---同步

目录

1.同步

1.1、有了互斥还为什么要有同步?

2.条件变量(实现同步的工具)

2.1、条件变量的使用原理:

2.2、条件变量的原理

3.条件变量接口

3.1、初始化接口:

3.2、等待接口:

3.3、唤醒接口:

4.条件变量夺命连环追问

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

4.2、那么条件变量等待接口是先让线程入等待队列呢?还是先解锁呢?

4.3、线程被唤醒之后会执行什么代码,为什么需要获取互斥锁?


1.同步

1.1、有了互斥还为什么要有同步?

  • 多个线程保证了互斥, 也就是保证了线程能够合理的访问临界资源了。但并不是说, 各个线程在访问临界资源的时候都是合理的。同步是为了保证多个线程对临界资源的访问的合理性,这个合理性建立在多个线程保证互斥的情况下。就比如说一个吃面的场景,当一个只有一个碗,吃面和做面的人都可以对这个碗进行访问操作,而对这个碗同时只能有一个人访问,不可以同时做面的人在往碗里做面吃面的人也在碗中吃面,要防止这样的情况发生就要采用互斥的原理,但是当有了互斥之后保证线程可以独自访问资源了,就如吃面的人可以独自吃面了而不会有做面的人来干扰,而做面的人也可以独自做面了,也不会有吃面的人同时和它抢生意,但是还有一个问题,就是你吃面的人不可以在碗里没有面的情况下去吃面,甚至把碗吃掉,你做面的人不可以在碗里有面的情况下,再去做面,那碗里都盛不下面了,所以此时要有同步的概念来保证访问临界资源的合理性。
  • 这里我们模拟一下上面的场景:

  • 运行结果:

2.条件变量(实现同步的工具)

2.1、条件变量的使用原理:

  • 线程在加锁之后,判断下临界资源是否可用(这里就像拿到碗之后,可不可以做面,就是当前碗里有没有面,如果没有面,那就可以做面,如果有面就不可以做面,将互斥锁放开然后进行等待,吃面也同理):
  • 如果可用:则直接访问临界资源
  • 如果不可用:则调用等待接口,让该线程进行等待
  • 反例代码演示:(但是这个程序是十分耗费cpu资源的,假设此时一个线程拿到的时间片是200ms,然后他在这个时间片内判断,吃面人如果当前碗里没有面那么他就continue退出这一次循环,但是此时它的时间片还没有结束,那么他就会再次加锁判断当前碗里面有没有面,但是还没有面,此时它又退出本次循环然后此时将锁锁上,但是此时它的时间片用完了,下一个线程要执行时拿到互斥锁,但是此时互斥锁是未被释放的,所以此时线程又要重复判断,但是在它的时间片耗之后互斥锁也是关闭的,所以判断是无效的,那么在这个时间片内这个进程干的事情就是无意义的,那么就会白白浪费时间,而也没有做事情。所以会导致效率低下)。

  • 运行结果:可以看到这里就正常了。做面人不会因为碗里有面再做面,吃面人不会因为碗里没有面而把碗吃掉

2.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.静态初始化:
    • pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    • PTHREAD_COND_INITIALIZER也是一个宏,和上面的静态初始化互斥锁那个宏类似。
  • 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、唤醒接口:

  • int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒PCB等待队列中所有的线程
  • int pthread_cond_signal(pthread_cond_t *cond);//唤醒PCB等待队列当中至少一个线程(有可能唤醒两个或者三个或者全部都唤醒)
  • 代码演示:

  • 运行结果:可以看到吃面人和做面人都在有条不紊的干着自己的事情

  • 这里我们将吃面人和做面人的数量增加到两个,各增加一位
  • 让代码跑起来我们发现此时吃面人和做面人的行为再次混乱做面的人在碗里有面的情况下还要做面,将面做到桌子外面(这里的运行结果就是,做面线程打印出来远远大于一碗面的数字,而吃面线程打印出来负数,碗里没有面了它还在吃,将碗也吃掉了)

  • 这里我们看调用栈可以看到这里一共5个线程,除了两个做面人和两个吃面人之外,还有主线程。
  • 这里我们来分析一下为什么会出现这种情况此时往里面有面(bowl==1)而其中一个吃面线程先将面吃掉,进入等待队列然后另一个吃面线程也将进入等待队列。
  • 此刻做面线程做了一碗面后将等待队列中两个线程都通知出来吃面,然后两个线程
  • 我们也可以用一个形象的场景来模拟一下这场景

  • 而我们要解决这个问题其实也很简单只要每次在被唤醒吃面(bowl--)时再次判断一下当前碗里有没有面就可以了
  • 我们虽然解决了刚刚多个吃面线程乱吃的问题,但是这里发现运行到最后程序不跑了

  • 那我们来看一下它的调用栈

  • 这样的一个场景就是所有线程都进入了等待队列中,但是没有人通知

  • 那么这种的情况是怎么形成的呢?

  • 总结:说到底产生这样的问题是因为所有做面线程已经进入等待队列当中去了,但是此时吃面进程欲要通知等待队列中的做面线程但是它有可能将吃面线程通知出来,导致所有线程进入等待状态。
  • 解决程序卡死方式:我们想要解决这个问题就要让做面线程每次唤醒的都是吃面线程,而吃面线程每次唤醒的都是做面线程。我们来写代码解决。(当然也可以用int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒PCB等待队列中所有的线程,但是种不推荐使用,因为太浪费CPU资源)

  • 运行结果:我们发现再也不会出现上面的卡死情况,和将碗吃了或者将面做在地上的情况。

4.条件变量夺命连环追问

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

  • 答案:这个问题其实很简单因为当你将线程放入等待队列中他就不会再执行下面的代码了,那么锁就会被永远锁上,其他线程就无法获取到这把锁来进行操作。所以我们要将互斥锁传入条件变量等待接口

4.2、那么条件变量等待接口是先让线程入等待队列呢?还是先解锁呢?

  • 答案:我们先假设是先解锁然后再让线程入等待队列,那么这样的话就可能导致我们先将锁打开了,现在刚刚好一个线程将锁拿到,然后修改了临界区资源而此时的临界区资源被修改后恰好满足条件,然后此时他唤醒等待队列当中等待的资源,但是现在等待队列当中还没有线程,但是然后他自己也进入等待队列当中进行等待,此时当此线程进入等待队列当中时之前的线程也刚刚到等待队列,那么此时两个线程就同时进入等待队列中。我们来模拟一下这个场景:

  • 总结:所以pthread_cond_wait在调用的时候先要将线程放入等待队列当中。然后再释放互斥锁。

4.3、线程被唤醒之后会执行什么代码,为什么需要获取互斥锁?

  • 答案:pthread_cond_wait在返回之前一定会在其内部进行加锁操作就是当一个线程在调用pthread_cond_wait函数进入等待队列中后,然后被唤醒时一定会在pthread_cond_wait函数中执行加锁操作。而在加锁操作时:
    • 抢到了:pthread_cond_wait函数就真正的执行完毕,函数返回。
    • 没抢到:pthread_cond_wait函数就没有被真正的执行完成,还处于函数内部抢锁的逻辑,然后一直来进行抢锁操作。

看到这里如果觉得对你有帮助不然点个赞吧!!!

猜你喜欢

转载自blog.csdn.net/weixin_45897952/article/details/124774635
今日推荐