多线程

2019-07-14 10:15发布


  线程是什么?
    Linux下没有真正的线程,因为linux下的线程是用进程pcb模拟的。所以linux下的线程也叫轻量级进程。
    既然Linux下pcb成了线程,Linux下的进程变成了线程组,所以进程的tgid(leader pid)就代替了原来的pid成为进程ID。
    linux下的pcb是线程,线程是CPU调度的基本单位。
    进程是操作系统资源分配的基本单位:运行一个程序的时候资源就会而完全分配,并且这些资源分配给线程组中的所有线程(因为他们共用这些资源),
     因为进程中的线程共享虚拟地址空间:共享代码段和数据段
 线程和进程:
    线程是CPU调度的基本单位,进程是资源分配的基本单位。
    Linux下的线程共用进程的虚拟地址空间:
       线程的创建/销毁成本更低
       线程的调度切换成本更低
       线程间的通信更加方便
       线程的执行力度更加细致
    线程的缺点:缺乏访问控制--线程安全
       有些系统调用和程序异常是针对整个进程产生影响。
       多个线程对临界资源进行操作会造成数据混乱。
    多线程/多进程
       io密集型程序---分摊等待
       cpu密集型程序--分摊计算
    线程共享:
       文件状态表
       信号处理方式
       用户ID和组ID
       当前工作目录
    线程独有:(相对独有--因为独有的数据还是在虚拟地址空间中)
       栈区,上下文数据,线程id,errno,信号屏蔽字
    进程和线程的优缺点:
        线程间通信非常方便,资源成本低,缺乏访问控制-线程安全需要考虑更多
        进程因为独立性 是所以健壮性比较强,通信比较麻烦,资源成本高。 
 线程控制:pthread_
    操作系统并没有提供相对应的系统调用来实现线程的接口,所以大佬们为线程的控制封装了一套线程库,使用这套接口创建的线程称之为用户态的线程
    但是它在操作系统内部对应了一个轻量级进程。
   线程创建:
       pthread_create
       通过参数返回一个用户态的线程id--是线程地址空间在进程虚拟地址空间中的首地址,这个用户态线程的操作都是围绕这个用户态的线程id来操作的
       pthread_self这个接口用于获取线程自身的ID。
       每一个线程都是一个task_struct也就意味着每个线程都有一个pid,但是ps命令只能显示一个task_struct中不仅有pid还有一个tgid
      (线程组id==线程组领导者的pid),那么ps看到的pid实际上是这个tgid(线程组ID),因此我们在linux下的进程成为了线程组
       查看线程信息可以使用ps -L命令 LWP这一列显示的就是线程pcb中的pid。 
   线程终止:
       不能在main函数中return,不能调用exit函数,因为这两个都是退出进程的,进程退出了,所有线程都得退出。
       pthread_exit:    谁调用谁退出    退出自己
       pthread_cancel:  取消其它线程    退出别人   返回-1
   线程等待:+2、‘’
       获取其他普通线程的退出返回值,避免产生僵尸线程
       pthread_join 只有线程处于joinable状态(线程默认属性),这个线程才能被等待。
       如果线程是被取消而退出的那么线程的退出返回值是-1
   线程分离: 
       线程退出后直接释放资源,无法获取返回值
       pthread_detach 设置线程的属性为分离属性,退出后直接释放资源。
       只有我们不关心线程的返回值的时候才设置,因为一旦设置就无法获取返回值了。
 线程安全:
     多个线程因为临界资源(公共资源)的争抢操作会导致程序逻辑的混乱或者数据的恶意性,因此就引入了线程安全的概念。
     但是线程的使用本身就是因为线程的通信方便以及成本低而广为使用,这样的话就无法避免大量临界资源的操作,
  这时候就必须要考虑如何保证线程的安全。
     保证线程的安全更多是保证数据的安全访问(互斥/同步)
线程间如何实现同步与互斥
 互斥:互斥锁---保证数据的同一时间的唯一访问性
    pthread_mutex_init  pthread_mutex_destroy   pthread_mutex_unlock
    pthread_mutex_lock  pthread_mutex_trylock   pthread_mutex_timedlock
   死锁:一个程序一直获取不到锁,因此一直处于卡死状态就可以称之为死锁
   死锁产生的四个必要条件:
       1.互斥条件:保证只能一个人拿到锁。
       2.请求与保持条件:拿到第一个锁去请求第二个锁的时候,拿不到但是不释放已获取的第一个锁
       3.不可剥夺条件:我的锁别人不能释放
       4.环路等待条件:我拿着我的锁去请求你的锁,你拿着你的锁来请求我的
   预防死锁就是打破必要条件即可
   避免死锁:死锁检测算法,银行家算法(调研)
 同步:条件变量---让数据的访问更加具有时序的可控性(等待,通知)
    满足操作条件,才可以操作,不满足则需要等待,而条件要满足就需要其他线程修改文件,并且通知一下等待的进程
    pthread_cond_init  pthread_cond_destroy       pthread_cond_broadcast
    pthread_cond_wait  pthread_cond_timedwait     pthread_cond_signal
    为什么条件变量要用到互斥锁?——保证对条件操作的安全性
    条件变量中的解锁和休眠必须是原子操作,并且在被唤醒后会将互斥锁的值修改为0
    条件的判断必须是循环(否则如果多个线程被同时唤醒,则对临界资源的访问不安全)
    互斥锁没有时序:因为1:没有条件判断  2:没有通知+
 生产者与消费者模型:
      功能:解耦,支持忙闲不均,支持并发
      一个场所,两类角 {MOD},三种关系
      场所指的是数据的存放位置,角 {MOD}值得是生产者和消费者,三种关系指的是如何保证生产者和消费者间数据安全的访问
      生产者与生产者互斥关系,消费者与消费者之间互斥关系,生产者与消费者同步关系。
 POSIX标准信号量:
     信号量的本质:就是一个计数器+具有等待队列的计数器(标注是否具有资源可以操作)
     功能:实现进程/线程间的同步与互斥
     sem_init  初始化一个信号量  
     sem_destroy 销毁一个信号量
     信号量的最常用的基本操作:
     sem_wait-获取信号量:判断计数器是否大于0,如果大于0,代表有资源可操作,对计数器进行-1,进而对临界资源进行操作,
                 如果计数<=0代表没有资源可供操作,那就挂起等待sem_wait
     sem_post-释放信号量:对信号量这个计数器进行+1操作,并且通知等待在信号量上的进程/线程
                 sem_post
 读写锁:默认读锁优先(读写锁是通过自选锁实现的:如果不具备加锁条件,则不会挂起会一直循环判断是否具备加锁条件
                                               如果操作时间过长,占用cpu资源会较长)
      _write
      _nr_read
      优先级:读锁优先
      写锁优先:加写锁,当时不一定能加成功,它会等待读者数量为0(为有读锁了),并且在这期间拒绝加读锁的请求
      pthread_rwlock_init   读写锁初始化
      pthread_rwlock_destroy 读写锁销毁
      pyhread_rwlock_rdlock  加读锁
      pthread_rwlock_wrlock  加写锁
      pthread_rwlock_unlock  解锁
     设置写锁优先:
          pthread_rwlockattr_init    写锁优先初始化
          pthread_rwlockattr_setkind_np   设置读写锁属性 
   读写锁的特性:
       1.写的时候别人既不能读,也不能写
       2.读的时候大家都能读,但是不能写
       3.默认读锁优先
   读写锁的使用场景:
      1.应用于写的操作少,读的操作多的场景
      2.不管是读操作还是写操作,操作时间都比较短
   读写锁的用自旋锁实现的:
      自旋锁是不断的发起加锁请求,如果等待时间过长非常占用cpu资源
  线程池:一大堆线程(线程+任务队列)
     接收到一个请求,然后启动一个线程处理这个请求,如果线程的创建和销毁成本相较于处理操作的时间可以忽略不计的话,那么没什么影响,
     但是如果处理请求时间和线程的创建销毁时间相差不多,那么创建和销毁一个线程的时间就是我们程序性能的杀手。
     创建一堆线程等待处理任务,接收到请求就选择一个线程去处理,那么这样就避免了大量的线程创建销毁成本。
     同时也避免大量且快速的请求到来,导致瞬间大量线程创建,耗尽资源导致程序运行出错崩溃。
    功能:
       避免大量线程创建/销毁成本,避免线程创建过多导致程序运行出错。
    1.启动时就创建固定数量线程,一直处理请求--请求较多,耗时较少。
    2.启动时不创建线程,当有请求的时候找空闲线程,如果没有空闲线程,就创建一个工作线程去处理请求,
      如果工作线程空闲等待超时时,则工作线程退出释放资源。   --请求一般情况较少,耗时较长,防止峰值压力 。 
 单例模式:对象只能初始化一次
       static T data
   饿汉/懒汉(更希望写懒汉模式)
   饿汉模式一次把所有需要的空间申请好,后面不再去申请空间
       if(data==NULL)
             data =new
   懒汉模式中需要注意的是线程安全,如果有需要用到再去申请空间
       lock
       if(data == NULL)
            data = new
       unlock
   调研:悲观锁,乐观锁,自选锁
   让性能更加好一点