Linux 多线程

目录

1. Linux线程概念

2. 线程控制

2.1 线程创建

2.2 线程终止

2.3 线程等待

2.4 线程分离

3. 线程安全

4. 同步与互斥

4.1 互斥锁

4.2 死锁

4.3 同步

4.4 生产者与消费者模型

4.3 posix标准信号量

4.4 读写锁(读写者模型)

5. 线程池

5.1 作用

6. 线程安全的单例模式


1. Linux线程概念

  • 因为Linux下线程以进程pcb模拟实现线程
    • Linux下pcb是线程,Linux线程也称为轻量级进程
      • 线程是cpu调度的基本单位
    • 进程是线程组,程序运行起来,资源是分配给整个线程组的
      • 进程是资源分配的基本单位
  • 线程是一个进程内部的控制序列
  • 一个进程中至少有一个线程 ,即 一个线程组中至少有一个线程
  • 线程之间不存在父子关系
  • 线程在进程内部运行 -> 共用同一块虚拟地址空间  ->  更加轻量化

  • 多进程与多线程都可以并行多任务,使用哪个?(结合使用场景,分析利弊)
    • 线程缺点
      • 线程之间缺乏访问控制  ->  系统调用exit以及异常针对的是整个进程 -> 健壮性低
      • 相较之下,多进程的独立性导致健壮性高
    • 线程优点
      • 一个进程中的线程共用同一个虚拟地址空间 
        • ->  线程间通信更加方便
        • ->  线程的创建/销毁成本更低
        • ->  线程间切换调度成本更低
        • ->  线程的执行粒度更细

  • 多进程/多线程进行多任务处理的优势体现与细节
    • cpu密集型程序
      • 大量处理数据、数据计算
    • io密集型程序
      • 大量的io操作

  • vfork()创建一个子进程共用同一个虚拟地址空间,怕出现调用栈混乱  ->  子进程运行完毕或程序替换后父进程才开始运行
  • 多线程使用同一个虚拟地址空间,如何实现同时运行而不出现调用栈混乱?
    • 为每个线程在虚拟地址空间中单独分配一块空间 --- 线程地址空间(在共享区)
    • 每个线程都会有一些独立的信息
      • 一组寄存器:有一套东西可以保存寄存器中的内容
      • errno
      • 信号屏蔽字 :屏蔽自己想屏蔽的信号
      • 调度优先级
    • 线程之间共享的数据
      • 共用同一个虚拟地址空间 ->  共享代码段和数据段
      • 文件描述符表
      • 每种信号的处理方式
      • 用户id,组id,当前工作路径

2. 线程控制

  • 2.1 线程创建

    • 操作系统并没有为用户提供直接创建线程的系统调用接口,于是有人封装了一套线程库实现线程控制
      • ->创建的线程是一个用户态线程,在内核中对应了一个轻量级进程实现程序的调度运行
    • 2.1.1 实现方式
      • int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
        • pthread.h  因为是库函数,因此编译链接需要加上-phread / -lpthread链接线程库
        • thread:输出型参数,获取新创建的线程id
        • attr:设置线程属性,通常置空
        • start_routine:线程入口函数
        • arg:通过线程入口函数传递给线程的参数
        • 返回值
          • 成功 -> 0
          • 失败 -> errno>0
    • ps -L:查看轻量级进程(线程)信息
    • 几个id
      • tid
        • 线程地址空间的首地址
      • task_struct-pid
        • LWP -> 轻量级进程id
      • task_struct-tgid
        • 线程组id
        • PID=主线程pid
  • 2.2 线程终止

    • 2.2.1 实现方式
      • 1. 在线程入口函数中return,可以退出
      • 2. 主动退出
        • void pthread_exit(void *retval);
          • 退出调用线程,retval作为返回值
      • 3. 被动退出
        • int pthread_cancel(pthread_t thread);
          • 取消指定线程 tid == thread
          • 如果一个线程是被取消,则返回值是一个宏:PTHREAD_CANCELED = -1
    • 2.2.2 注意
      • 主线程退出,进程并不会退出;所有线程退出后,进程才会退出
      • 线程退出也会成为僵尸线程,但是普通线程体现不出效果
        • 线程地址空间无法被回收再利用,造成内存泄漏
  • 2.3 线程等待

    • 等待指定线程退出 -> 获取指定线程的返回值,允许系统回收线程资源 -> 避免资源泄露、避免僵尸进程
    • 2.3.1 前提
      • 线程创建出来,默认有一个属性:joinable
        • 这个属性决定了线程退出后,必须被等待;否则线程退出后,为了保存退出返回值,不会自动回收线程的资源,成为僵尸线程(无直观体现)
      • 线程能够被等待
    • 2.3.2 实现方式
      • int pthread_join(pthread_t thread, void **retval);
        • thread:要等待的线程
        • retval:输出型参数,用于获取退出线程的返回值
  • 2.4 线程分离

    • 将线程的一个属性从joinable设置为detach属性
    • 被分离的线程,处于detach属性的线程,退出后资源直接自动被回收不能被等待,等待会报错
    • 2.4.1 实现方式
      • 通常如果用户对线程的返回值并不关心,则在创建线程之后直接分离线程或在线程入口函数中第一时间分离自己
      • int pthread_detach(pthread_t thread);
        • 分离一个指定的线程(也可分离自己)
        • 线程的分离,对于一个线程来说,任意线程在任意位置调用都可以

3. 线程安全

多个线程同时操作临界资源而不会出现数据二义性。

  • 可重入/ 不可重入:多个执行流中是否可以同时进入函数运行而不会出现问题
  • 如何判断线程安全:在线程中是否对临界资源进行了非原子操作.
  • 如何实现线程安全:同步与互斥

4. 同步与互斥

  • 同步
    • 资源访问的合理性 -> 临界资源的合理访问
  • 互斥
    • 资源访问的安全性 -> 临界资源同一时间唯一访问
    • 通过互斥锁实现
  • 4.1 互斥锁

    • 本质上是一个0/1计数器,本身的操作是原子的
    • 加锁避免边生产边消费
    • 4.1.1 加锁
      • 1表示可以加锁,加锁计数-1
      • 0表示不可加锁,不可加锁则等待
    • 4.1.2 解锁
      • 加锁后解锁,计数+1
    • 4.1.3 互斥锁操作步骤
      • 1. 定义互斥锁变量         
        • pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
      • 2. 初始化互斥锁变量     
        • int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
          • mutex:互斥锁变量
          • attr:属性,通常置空
      • 3. 加锁 / 解锁   
        • 加锁要在临界资源访问之前
          • 阻塞加锁:无法加锁就阻塞
            • int pthread_mutex_lock(pthread_mutex_t *mutex);  
          • 非阻塞加锁:无法加锁则直接报错返回
            • int pthread_mutex_trylock(pthread_mutex_t *mutex);
          • 限时的阻塞加锁
            •  int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
        • 加锁后,需要在线程任意有可能退出的地方进行解锁
          • int pthread_mutex_unlock(pthread_mutex_t *mutex);
      • 4. 销毁互斥锁                
        • int pthread_mutex_destroy(pthread_mutex_t *mutex);
          • mutex:互斥锁变量
    • 4.1.4 注意
      • 互斥锁变量不一定非要全局变量,只要保证要互斥的线程都能访问到即可
  • 4.2 死锁

    • 因为对一些无法加锁的锁,进行加锁而导致程序卡死
    • 4.2.1 死锁产生的必要条件 (面试)
      • 互斥条件:我操作时别人不能操作
      • 不可剥夺条件:我的锁,别人不能释放
      • 请求与保持条件:拿着手里的,请求其他的。其他的请求不到,手里的也不放。
      • 环路等待条件
    • 4.2.2 产生场景
      • 加锁 / 解锁顺序不同
    • 4.2.3 如何预防死锁
      • 破坏必要条件
    • 4.2.4 如何避免死锁
      • 死锁检验算法
      • 银行家算法
  • 4.3 同步

    • 4.3.1 同步的实现  等待+唤醒
      • 临界资源访问合理性 -> 生产出来才能使用;没有资源则等待(死等),生产资源后唤醒等待
      • 条件变量实现同步步骤
        • 1. 定义条件变量
          • pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
        • 2. 初始化条件变量
          •  int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
            • cond:条件变量
            • attr:属性,通常置NULL
        • 3. 等待 / 唤醒
          • 等待
            • -> 条件变量与互斥锁是搭配使用的
              • 因为条件变量本身并不具备操作条件判断的功能,对条件的判断是一个临界资源操作,需要加锁
            • 限时等待,超时后返回
              • int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                              pthread_mutex_t *restrict mutex,
                              const struct timespec *restrict abstime);
                • cond:条件变量
                • mutex:互斥锁
                • abstime:限时等待时长
            • 死等
              • int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
                • cond:条件变量
                • mutex:互斥锁
                • 集合了解锁+休眠+被唤醒后加锁的原子操作
          • 广播唤醒:唤醒所有等待的人
            • int pthread_cond_signal(pthread_cond_t *cond);
              • 唤醒至少一个等待的人
              • cond:条件变量
        • 4. 销毁条件变量
          • int pthread_cond_destroy(pthread_cond_t *cond);
    • 4.3.2 注意
      • 条件需要循环判断
      • 若对临界资源操作的线程有多个角色,需要分别等待多个不同的条件变量,并且分别进行唤醒,避免出现唤醒角色错误问题。
  • 4.4 生产者与消费者模型

    • 4.4.1 模型
      • ​​​​​​​一个场所,两个角色,三种关系
        • 多个生产者,多个消费者,多个产品
        • 多个生产者生产产品,存放于队列中
        • 多个消费者消费产品,从队列中获取
    • 4.4.2 生产者与消费者之间如何保证安全操作?​​​​​​​
      • 向队列中同时存放数据或同时从队列中取出数据可能会出现问题
      • 为保证安全操作,需要保证
        • 生产者之间是互斥关系
        • 消费者之间是互斥关系
        • 生产者与消费者之间是同步+互斥关系
    • 4.4.3 代码实现
    • 4.4.4 作用
      • ​​​​​​​解耦合
      • 支持忙前不均,起到缓冲作用
      • 支持并发
  • 4.3 posix标准信号量

    • 4.3.1 信号量
      • 具有等待队列的计数器 -> 主要进行资源计数+等待+唤醒功能
      • 主要用于实现进程 / 线程间同步
      • ​​​​​​​可以初始一个资源计数
      • 当获取资源时,先判断计数
        • 若>0,表示有资源,则计数-1,直接返回,获取资源
        • 若<=0,表示没有资源,则阻塞等待
      • 当产生资源时,计数+1,唤醒等待队列上的进程 / 线程
    • 4.3.2 信号量与条件变量的区别
      • 条件变量:依靠外部用户完成条件的判断
      • 信号量:自身内部的计数器完成条件的判断
    • 4.3.3 posix标准信号量接口操作步骤
      • ​​​​​​​1. 初始化信号量
        • int sem_init(sem_t *sem, int pshared, unsigned int value);
          • sem:信号量
          • pshared
            • 0:用于线程间同步与互斥
            • !0:用于进程间同步与互斥
          • value:信号量的计数初值
      • 2. 计数-1,之后计数<=0,则等待;否则直接返回
        • ​​​​​​​int sem_wait(sem_t *sem);
          • ​​​​​​​计数判断
            • 若<=0,则阻塞等待
        • int sem_trywait(sem_t *sem);
          • ​​​​​​​计数判断
            • 若<=0,则报错返回
        • int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
          • ​​​​​​​计数判断
            • 若<=0,则限时等待
      • 3. 若资源产出,计数+1,唤醒等待
        • ​​​​​​​​​​​​​​int sem_post(sem_t *sem);
          • ​​​​​​​唤醒sem信号量等待队列上的线程
      • 4. 销毁信号量
        • ​​​​​​​int sem_destroy(sem_t *sem);
          • ​​​​​​​销毁信号量
  • 4.4 读写锁(读写者模型)

    • 写互斥,读共享
      • 写的时候,其他人不能读也不能写;读的时候,可以同时读,但是不能写。
      • 若write_count > 0,读锁写锁都阻塞
      • 若read_count > 0,写锁阻塞,可以加读锁
    • 4.4.1 自旋锁
      • 读写锁加锁时的阻塞,使用自旋锁实现 -> 循环对资源进行判断(过一段时间判断一次)
      • 自旋锁使用场景:CPU消耗较大,操作时间很短
    • 4.4.2 常用接口
      • 读写锁定义
        • pthread_rwlock_t
      • 初始化读写锁
        •  int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
          • rwlock:读写锁
          • attr:读写锁属性
      • 加读锁
        • int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
          • 阻塞加读锁
        • int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
          • 非阻塞加读锁
      • 加写锁
        • int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
          • 阻塞加写锁
        •  int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
          • 非阻塞加写锁
      • 解锁
        • int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
      • 销毁读写锁
        •  int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
          • rwlock:读写锁
    • 4.4.3 读写锁的优先级
      • 读写锁通常用于读者多,写者少的情况;这时想要加写锁修改数据,但是一直有线程 / 进程加读锁,导致饥饿问题
      • 因此,读写锁是有优先级的
        • 写优先
          • ​​​​​​​若加写锁加不上,在等待期间,拒绝后续的加读锁操作
        • 读优先(默认)
          • 若加读锁加不上,在等待期间,拒绝后序的加写锁操作
      • 当需要写优先(读者多,写者少)时,需要设置读写锁属性
        • int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
          • 销毁读写锁属性
        • int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
          • 初始化读写锁属性
        • int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref); 
          • 设置读写锁优先级
            • PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
            • PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和PTHREAD_RWLOCK_PREFER_READER_NP 一致
            • PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁 

5. 线程池

有最大数量上限的一堆线程+线程安全的任务队列

线程池中的线程从人物队列中获取任务,然后进行处理。

  • 5.1 作用

    • 1. 避免大朗线程创建 / 销毁的时间成本
    • 2. 避免峰值压力,导致线程创建过多,资源耗尽,程序崩溃
    • 3. 解耦合
    • 4. 支持忙前不均
    • 5. 支持并发
  • 5.2 如何实现
    • 1. 线程的创建(线程池中的线程有最大数量限制)
    • 2. 线程安全的任务队列的实现
    • 3. 向队列添加任务

6. 线程安全的单例模式

  • 单例模式:典型设计模式中的一种,一个对象只能被初始化一次。
    • 饿汉模式
      • 启动阶段一次性初始化完毕
      • 用户体验不好,程序初始化事件可能较长,但是运行流畅
    • 懒汉模式
      • 将初始化分摊到各个阶段,使用时初始化,启动节点
      • 用户体验较好,但是运行到某个模块时,流畅度不太好
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值