Linux线程安全之---信号量

目录

1.信号量:

1.1、信号量的原理

1.2、信号量的接口

1.2.1、初始化接口:

1.2.2、等待接口:

1.2.3、释放接口:

1.2.4、销毁接口:

2.生产者与消费者信号量代码实现:

3.线程池:

3.1、应用场景

3.2、原理:

3.3、构造线程池要完成的事情

3.4、代码实现

4.读写锁

4.1、应用场景:

4.2、读写锁的三种状态

4.3、读写锁的接口:

面试题:


1.信号量:

  • 1.1、信号量的原理

  • 资源计数器+PCB等待队列
    • 资源计数器:
      • 执行流获取信号量,
      • 获取成功:信号量计数器减一操作
      • 获取失败:执行流放入到PCB等待队列
    • 执行流释放信号量成功之后,计数器加一操作。
  • 1.2、信号量的接口

  • 1.2.1、初始化接口:

    • int sem_init(sem_t *sem,int pshared,unsigned int value);

    • 参数:
    • sem :信号量,sem_t是信号量的类型(sem_t是一个结构体其中有资源计数器和PCB等待队列)
    • pshared :该信号量是用于线程间还是用于进程间
      • 0 :用于线程间,全局变量

      • 非0 :用于进程间,将信号量所用到的资源在共享内存当中进行开辟

    • value :资源的个数,初始化信号量计数器的
  • 1.2.2、等待接口:

    • (先获取信号量,再获取互斥锁,先获取信号量,再保证互斥。就是说接口一定是先对程序计数器进行减一操作,然后再拿到锁。假设如果是先拿到锁之后再进行信号量减一,那么当拿到锁之后,当前的信号量为0然后减一信号量小于0,则执行流放到等待队列中,而这个等待接口中也没有传入互斥锁所以内部不会解锁,所以这时线程就会带着锁进入等待队列,然后无法解锁,锁一直被锁着,其他临界区也无法访问当前临界区资源。)
      • int sem_wait(sem_t *sem);
      • 1.对资源计数器进行减一操作——————p操作。(信号量的计数器自己保证原子性操作不会因为多线程而导致程序计数器中的结果二义性减一操作一步完成。)
      • 2.判断资源计数器的值是否小于0
        • 是:则阻塞等待,将执行流放到PCB等待队列中
        • 不是:则接口返回
      • 总结:就是说当一个线程调用这个函数,拿到信号量之后它会直接先将当前资源计数器进行减一操作,然后再判断当前资源计数器是否小于0,当小于0时就将当前线程放入PCB等待队列当中,如果不是则接口返回,执行要对临界资源的操作。
  • 1.2.3、释放接口:

    • int sem_post(sem_t *sem);
    • 1.会对资源计数器进行加一操作。——————-v操作
    • 2.判断资源计数器的值是否小于等于0(这里0还要通知是因为,假设有一个生产者队列和一个消费者队列,当生产者将队列生产满了之后,它的程序计数器为-1而将程序计数器减为-1的那个线程还在等待队列当中,此时当消费者消费一个数据时也就是进行出队操作时将生产者当中的信号量加一变为0,那么此时要通知等待队列当中的线程出来工作)
      • 是:通知PCB等待队列
      • 否:不用通知PCB等待队列,因为没有线程在等待。
    • 信号量模型:

  • 1.2.4、销毁接口:

    • int sem_destroy(sem_t *sem);
    • 传入信号量sem然后销毁。

2.生产者与消费者信号量代码实现:

  • 运行结果:

3.线程池:

  • 3.1、应用场景

    • 3.1.1、一个线程被创建之后,只能执行一个线程入口函数,后续是没有办法更改的,基于这种场景,线程可能执行的代码也就是固定了。换句话说即使线程入口函数当中有很多分支,但是相对来说线程执行的路线都是固定的,要么时A分支,要么时B分支,要么是C分支。这里的分支是指类似if,else语句。这里如果我们要是在后续再想新增加新的业务判断逻辑,那就只能在原有线程入口函数进行增加写代码,这样就会导致一个线程入口函数的代码愈来愈多。那么如果代码的耦合性过高,只要一个地方出现错误,我们查找bug时就会十分头疼。
    • 3.1.2、所以为了能让线程执行不同的业务代码,就要考虑线程从队列中获取元素的身上下功夫。让线程可以通过线程元素来执行不同的代码。
  • 3.2、原理:

  • 3.3、构造线程池要完成的事情

    • 1.创建固定数量的线程池,循环从任务队列中获取任务对象。
    • 2.获取到任务对象之后执行任务对象的任务接口。
  • 3.4、代码实现

4.读写锁

  • 4.1、应用场景:

    • 大量读取,少量写的场景。
    • 允许多个线程并行读,多个线程互斥写
  • 4.2、读写锁的三种状态

    • 以读模式加锁的状态
    • 以写模式加锁的状态(互斥锁)
    • 不加锁的状态
  • 4.3、读写锁的接口:

    • 初始化接口:
    • int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
    • 参数:
      • pthread_rwlock_t:读写锁的类型
      • rwlock:传递读写锁
      • attr:NULL,默认属性
    • 销毁接口:
    • int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    • 以读模式进行加锁:
    • int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);(阻塞接口)
    • 允许多个线程并行以镀膜室获取读写锁。
    • 引用计数:用来记录当前读写锁有多少个线程以镀膜室获取了读写锁。
      • 1.每当有线程以读模式进行加锁,引用计数++;
      • 2.每当读模式的线程释放锁,引用计数--;
    • 引用计数的作用,当引用计数为0时,那么证明当前没有线程在进行读取操作,那么写的线程就可以获取到这把读写锁进行写。
    • int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);(非阻塞接口)
    • 以写模式进行加锁:
    • int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);(非阻塞接口)
    • int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);(阻塞接口)
    • int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);(解锁接口)
  • 面试题:

    • 现在如果有线程A和线程B在对一个资源进行读取,此时来了一个线程C要以写的方式获取这把读写锁,然后又来了一个线程D要进行读取,这里问线程C要不要等待线程D也读取完之后再对资源进行写?
    • 答案:这里读写锁的内部是有个机制的:当要进行写的线程对读写锁要进行操作的时候,这时要后面再来的要读取的线程就无法获取这把读写锁,要等待写的线程对资源操作完成之后他才可以进行获取资源。因为如果这时读取的线程可以获取这把锁,读写锁本来就是大量读少量写的使用场景,那么就会导致写的线程一直拿不到这把锁,这是不合理的。线程就会饿死。

看到这里如果觉得有用不如就点个赞再走吧!!!

猜你喜欢

转载自blog.csdn.net/weixin_45897952/article/details/124775001