同步&条件变量

目录

条件变量:

条件变量接口:

条件变量更深层次的理解(pthread_cond_wait内部原理)


问题引入:

  • 但现象是,要么吃面线程一直吃面,要么做面线程一直做面,而不是,碗里没面做面线程往碗里做了一碗面,碗里有面吃面线程就把碗里的面吃了。
  • 多个线程保证了互斥,也就是保证了线程能够独占访问临界资源了。但并不是说,各个线程在访问临界资源的时候都是合理的,同步是为了保证多个线程对临界资源的访问的合理性,这个合理性建立在多个线程保证互斥的情况下。
  • 对资源加以判断后
    • 结果

    • 出现上面结果的原因
      • 在单核CPU环境下,两个线程CPU拿到运行时,会给线程分配时间片,由于时间片远大于线程执行一次循环的时间,以吃面线程为例,假设碗里有面(g_bowl == 1),吃面线程抢到锁,吃面线程把面吃了(g_bowl - - ),然后解锁,可是吃面线程的时间片还没到,可以继续执行自己的代码,再次进入while循环,吃面线程再次抢到锁,可是此时碗里没面了(g_bowl == 0),它就会执行判断语句,打印内容,然后解锁,可是吃面线程的时间片还没到,再次进入while循环,可碗里还是没面(g_bowl == 0),再次执行判断语句,打印内容,然后解锁。。。。。。直到时间片用完,被剥离CPU,做面线程才有可能被调度,运行自己的代码,然后抢锁,往下执行。
    • 当前写的这个代码,非常耗费CPU资源,因为判断了即使资源没有准备充分,释放掉互斥锁之后,也极有可能这个互斥锁又被该线程拿到。

条件变量:

  • 条件变量的使用原理:
    • 线程在加锁之后,判脚下临界资源是否可用:
      • 如果可用:则直接访间临界资源
      • 如果不可用:则调用等待接口,让该线程进行等待
  • 条件变量的原理
    • 本质上是:PCB等待队列(存放在等待的线程的PCB)

条件变量接口:

  • 初始化接口:
    • 动态初始化
      • int pthread _cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
      • pthread_ cond_t :条件变量类型(一个结构体)
      • 参数:
        • cond
          • 接收一个条件变量的指针(接收一个条件变量的地址)
        • attr:
          • 表示条件变量的属性信息,传递NULL,采用默认属性
    • 条件变量的销毁
      • int pthread_cond_destroy(pthread_cond_t *cond);
    • 静态初始化:
      • pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 等待接口:
    • int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
      • 谁(线程)调用等待接口,就将谁放到条件变量对应的PCB等待队列当中
      • 参数:
        • cond :条件变量
        • mutex:互斥锁
  • 唤醒接口:
    • int pthread_cond_broadcast(pthread_cond_t *cong);
      • 唤醒PCB等待队列当中的所有线程
    • int pthread_cond_signal(pthread_cond_t *cond),
      • 唤醒PCB等待都一列当中至少一个线程
  • 吃面线程和做面线程使用条件条件变量后:
    • 终于达到了预想的结果,吃面线程吃一碗面,做面线程做一碗面
  • 但是,增加吃面线程和做面线程的个数后:
    • 2个吃面线程,2个做面线程
    • 结果是要么某个吃面线程一直吃面,要么某个做面线程一直做面,
    • 这个问题先放一放,再对条件变量往深层次理解

条件变量更深层次的理解(pthread_cond_wait内部原理)

  • 1.条件变量的等待接口第二个参数为什么会有互斥锁?
    • 这个问题用一个吃面线程和一个做面线程做假设
    • 假设吃面线程先拿到互斥锁,且g _bowl == 0(碗里没面),假设在调用函数pthread_cond_wait()之后,函数内部没有解锁逻辑,则当吃面线程被放到条件变量对应的PCB等待队列中时,互斥锁也被它带走了,那么做面线程永远也拿不到锁,就会一直阻塞在加锁接口,无法往下执行,也就不会++g_bowl,更不会通知吃面线程吃面,这样就会导致整个进程卡死了,因此,在函数pthread_cond_wait()内部,会有解锁的逻辑。
    • 1.在线程访问临界资源之前,一定是加锁访问的,保证互斥属性。2.传递互斥锁给pthread_cond_wait()接口,就是想让当前线程放到条件变量对应的PCB等待队列中时可以进行解锁。
  • 2.ptrhead_cond_wait的内部是先释放互斥锁还是先将线程放入到PCB等待队列?
    • 这个问题还是用一个吃面线程和一个做面线程做假设
      • 假设吃面线程先拿到互斥锁,且g _bowl == 0(碗里没面),然后就会调用函数pthread_cond_wait(),假设是先解锁,再将线程放入到条件变量对应的PCB等待队列中,则吃面线程刚解完锁,做面线程就立即抢到锁,然后就++g_bowl,并通知等待队列当中的吃面线程来吃面,但是吃面线程还没有被放到条件变量对应的PCB等待队列中,也就意味着,做面线程去通知条件变量对应的PCB等待队列的时候,该队列还是一个空队列,当吃面被放到条件变量对应的PCB等待队列中时,做面线程再次拿到锁,然后判断,发现g_bowl == 1,他也就被放到条件变量对应的PCB等待队列中,此时,两个线程在条件变量对应的PCB等待队列中,就再也没有线程来通知他们,就会导致整个进程卡死。因此,假设不成立。
    • 结论:先放到PCB等待队列,再进行解锁
  • 3.线程被唤醒了之后,需要再获取互斥锁吗?
    • pthread_cond_wait函数在返回之前一定会在其内部进行加锁操作;
    • 如果pthread_cond_wait函数在返回之前没有加锁操作,那么pthread_cond_wait函数在返回之后,该线程就会存有在不加锁的情况下访问临界资源,也就是不保证互斥访问,就会造成线程不安全现象。因此pthread_cond_wait函数在返回之前一定会在其内部进行加锁操作。
    • 抢锁的时候:
      • 1.抢到了,pthread_cond_wait函数就真正执行完毕了,函数返回。
      • 2.没抢到,pthread_cond_wait函数的代码就没有真正的执行完毕,还处于函数内部抢锁的逻辑当中,还会继续去抢锁,直到抢到互斥锁,才返回。
  • 现在可以解决上面遗留的问题了(画图不是太方面,就以文字来描述一下)
    • 现在有4个线程,吃面线程1,吃面线程2,做面线程1,做面线程2,假设吃面线程1先拿到互斥锁,且g _bowl == 0(碗里没面),调用函数pthread_cond_wait()后,先放到PCB等待队列,再进行解锁,此时其他三个线程都有可能会抢到锁,假设吃面线程2先拿到互斥锁,也会被放到PCB等待队列,然后解锁,此时枪锁的只有另外两个做面线程,假设做面线程1先抢到锁,它发现g_bowl == 0(碗里没面),他就会++g_bowl(做一碗面),然后解锁,然后通知等待队列中的两个吃面线程,它一通知,就有可能把两个吃面线程从等待队列中通知出来,假设此时吃面线程1先抢到互斥锁,pthread_cond_wait()函数返回,按照代码逻辑,吃面线程1就会往下执行,g_bowl--(吃面),此时g_bowl == 0,然后解锁,刚解锁,被通知出来吃面的吃面线程2就立即抢到锁,pthread_cond_wait()函数返回,按照代码逻辑,吃面线程2就会往下执行,g_bowl--(吃面),此时g_bowl == -1,然后解锁,刚解锁,如果又被吃面线程中的某一个抢到锁,由于此时g_bowl已经小于0,不会再执行判断语句,然后就g_bowl--(吃面),解锁,又被吃面线程中的某一个抢到锁,就这样,会在结果中看到负数,而看到很大的正数的场景和这个类似,就不再展开。
    • 如何解决呢:
      • 当pthread_cond_wait()返回之后,再次判断临界资源是否可用
    • 但是,这又引入了新的问题:
    • 改进后的运行结果如下:
      • 查看调用堆栈后发现:4个工作线程都阻塞在pthread_cond_wait()接口
      • 原因分析:4个工作线程最终都被放到了条件变量对应的PCB等待队列中,再没有线程去唤醒PCB等待队列中的4个线程
      • 对出现这种场景做一个详细分析:现在有4个线程,吃面线程1,吃面线程2,做面线程1,做面线程2,假设吃面线程1先拿到互斥锁,且g _bowl == 0(碗里没面),调用函数pthread_cond_wait()后,先将自己放到PCB等待队列,然后解锁,此时其他三个线程都有可能会抢到锁,假设吃面线程2先抢到互斥锁,也会被放到PCB等待队列,然后解锁,此时抢锁的只有另外两个做面线程,假设做面线程1先抢到锁,它发现g_bowl == 0(碗里没面),他就会++g_bowl(做一碗面),g_bowl由0变为1,然后解锁,然后去唤醒等待队列中的吃面线程,然而这次它只唤醒了其中的一个吃面线程,假设唤醒的是吃面线程2,但是吃面线程2的pthread_cond_wait()还没有真正返回,还在抢锁阶段,就在这个时候,锁被做面线程2抢到了,它访问临界资源,发现g_bowl == 1,就会调用函数pthread_cond_wait(),将自己放到PCB等待队列,然后解锁,可是被唤醒的吃面线程2还是没有抢到锁,锁被做面线程1先抢到了,它访问临界资源,发现g_bowl == 1,就会调用函数pthread_cond_wait(),将自己放到PCB等待队列,然后解锁,现在,被唤醒的吃面线程2终于抢到锁了,它访问临界资源,发现g_bowl == 1,可以使用,然后就g_bowl--(吃面),g_bowl由1变为0,然后解锁,然后去唤醒等待队列中的3个线程,但是它只唤醒了3个线程中的吃面线程1,另外2个做面线程没有被唤醒,被唤醒的吃面线程1拿到锁,访问临界资源,发现g_bowl == 0,临界资源不可用,就会调用函数pthread_cond_wait(),将自己放到PCB等待队列,然后解锁,此时能拿到锁的只有吃面线程2,他拿到锁,访问临界资源,发现g_bowl == 0,临界资源不可用,就会调用函数pthread_cond_wait(),将自己放到PCB等待队列,然后解锁,此时,4个线程都被放到了PCB等待队列,再没有线程去唤醒他们。
      • 解决方案1:分别给吃面线程和做面线程创建相应的条件变量
        • 吃面线程有自己的条件变量,做面线程有自己的条件变量
        • 吃面线程发现g _bowl == 0(碗里没面),就将自己放到吃面线程的条件变量对应的PCB等待队列当中。
        • 当吃面线程吃了一碗面,则通知做面线程的条件变量的PCB等待队列。
        • 做面线程发现g _bowl == 1(碗里有面),就将自己放到做面线程的条件变量对应的PCB等待队列当中。
        • 当做面线程做了一碗面,则通知吃面线程的条件变量的PCB等待队列。
      • 解决方案2:
        • 唤醒接口使用pthread_cond_broadcast(pthread_cond_t *cong),唤醒PCB等待队列当中的所有线程,但这样会浪费CPU资源,不推荐
    • 吃面线程和做面线程终极版本:
      • 运行结果:

猜你喜欢

转载自blog.csdn.net/sy2453/article/details/124694663
今日推荐