线程

原文地址: https://sunnyqjm.github.io/2018/06/27/os_02/


线程

线程CPU使用的基本单元,它由线程ID、程序计数器,寄存器集合和栈组成。它与属于同一个进程的其它线程共享代码段、数据段和其它操作系统资源,如打开文件和信号。

线程_第1张图片
单线程进程和多线程进程
  • 线程的优点

    • 响应度高:交互式应用程序采用多线程实现的话,可以一部分线程处理用户I/O等耗时操作,而一部分用来响应用户的操作。这就不会出现用户一次交互之后系统由于等待I/O而卡住的情况,可交互性大大提升。

    • 资源共享:同一个进程的线程间共享进程的内存和资源,线程之间的交互也比进程间交互要容易的多

    • 经济:创建一个线程所需要的开销远比创建一个进程的开销要小,所以相对于创建进程来说,创建一个线程是更为经济的。

    • 多处理器体系结构的应用:对于单线程进程,即便硬件有多个处理器,一个进程同一时刻只能在一个处理器上运行。而对于一个多线程进程可以将线程同时运行在多个处理器上,这样就充分的发挥了多处理器的好处,加强了并发功能

  • 用户线程和内核线程

    参考:线程的3种实现方式--内核级线程, 用户级线程和混合型线程

    • 用户线程

      • 用户线程存在于用户空间由线程库统一管理对内核透明(也就是说内核并不知道有用户线程的存在)

      • 用户线程的创建和销毁均在用户空间由线程库实现,无需内核干预,不需要调用系统调用,也就不会有用户态与内核态相互切换的开销

      • 用户线程不具有自己的上下文,不能直接参与CPU的竞争,必须关联一个内核线程,由内核线程去竞争CPU资源(操作系统是以内核线程为最基本的单位,分配CPU资源的

    • 内核线程

      • 内核线程存在于内核空间,由操作系统管理

      • 内核线程的创建和销毁都是通过系统调用来完成的

      • 内核线程是操作系统分配CPU资源的最小单位(也就是说内核线程是直接参与CPU竞争的)=> 每次调度器选择一个内核级线程,为其分配CPU的使用权(比如说可以是时间片)

多线程模型

  • 多对一模型
  • 一对一模型
  • 多对一模型
  • 多对一模型

    一个进程的所有用户线程都映射到同一个内核线程上线程的管理交由线程库

    线程_第2张图片
    多对一模型
    • 内核的视角来看,进程仍然是一个单线程进程,用户线程对内核是透明的

    • 因为都映射到同一个内核线程,所以同一时刻只有一个线程能访问内核,也就是说同一时刻只能有一个线程可以调用系统调用(如果此时某个线程执行了一个阻塞的系统调用,那么同进程的其它线程都将被阻塞

    • 因为线程的管理都是由线程库在用户空间完成的,不涉及系统调用,所以效率会比较高

  • 一对一模型

    每个用户线程都映射一个内核线程

    线程_第3张图片
    一对一模型
    • 由于每个用户线程各自映射一个内核线程,所以当一个线程执行阻塞系统调用的时候,同进程的其它线程并不受其影响。=> 提供了比较好的并发功能

    • 允许同进程的多个线程运行在多个处理器上

    • 唯一的缺点就是,内核线程的创建是开销比较大的,过多的创建会影响系统的性能,所以导致所能创建的线程数量也是大受限制

  • 多对多模型

    多对多模型 克服了多对一模型并发性不行以及一对一模型所能创建线程数量受限的问题。

    [图片上传失败...(image-7182ff-1531380108354)]

    • 开发人员可以创建任意多的用户线程,并且相应的内核线程能在多处理系统上并发执行

    • 当一个线程执行阻塞系统调用的时候,内核能调度另一个线程来执行

  • 二级模型

    是上述一对一模型和多对多模型的变种。

    线程_第4张图片
    二级模型

线程库

线程库为程序员提供创建和管理线程的API

  • 用户级线程库和内核级线程库

    • 用户级线程库

      用户级线程库的实现是在用户空间中提供一个没有内核支持的库,此库的所有代码和数据结构都存在于用户空间当中。调用库中的一个函数只是导致用户空间一个本地函数的调用,而不是系统调用

    • 内核级线程库

      内核级线程库的实现方法是执行一个由操作系统直接支持的内核级的库库的代码和数据结构存在于内核空间当中调用一个库的API函数通常会导致对内核的系统调用

  • 常用线程库

    • POSIX Pthread

      扩展自POSIX标准,可以提供用户级或内核级的库
    • Win32

      适用于Windows操作系统的内核级线程库
    • Java

      Java线程API通常采用宿主系统上的线程库来实现。意味着在Windows系统上,Java线程通常采用Win32 API实现,而在Unix和LInux系统上采用Pthread。

多线程相关问题

  • 系统调用fork()和exec()

    通常,如果在调用fork之后立即调用exec函数,那么只复制当前线程(因为新的程序会替换掉整个进程);如果在调用fork函数之后不调用exec函数,那么应复制所有线程

    • fork()

      通常在线程内执行fork函数时,系统有两种选择。一种是复制当前进程的所有线程到新进程;另一种是只复制调用fork()的那个线程
    • exec()

      如果一个线程调用了exec()系统调用,那么exec()所启动的新的程序会替换当前的整个进程,包括所有的线程
  • 线程取消

    线程取消(thread cancellation)指的是在线程完成之前终止线程的任务

    • 异步取消asynchronous cancellation):一个线程立即终止目标线程
    • 延迟取消deferred cancellation):目标线程不断自检是否应该终止,这允许线程有机会以有序的方式来终止自己。
  • 信号处理

    信号 在Unix中用来 通知进程某个特定事件发生了

    • 信号的共同特征

      • 信号是由特定事件的发生所产生
      • 产生的信号要发送到进程
      • 一旦发送,信号必须加以处理
    • 同步信号和异步信号

      • 同步信号:同步信号由正在运行的进程产生并发送给自己处理
      • 异步信号:异步信号通常是由运行进程之外的其它进程产生,发送给本进程处理。
    • 对于多线程程序,信号发给谁?

      • 发送信号到信号所应用的线程
      • 发送信号到进程内的每个线程
      • 发送信号到进程内的某些固定线程
      • 规定一个特定线程以接收进程的所有信号

      通常,对于Unix系统,允许线程描述它会接收或者拒绝哪些信号。因此,有时一个异步的信号只能发送给那些不拒绝它的线程

    • 信号的处理?

      • 每个信号都有一个默认信号处理程序default signal handler),如果不作额外的处理,每当信号来临,就会执行信号对应的默认处理程序
      • 当然,这种默认动作也可以通过用户自定义一个信号处理程序来覆盖默认行为。一旦定义了某个信号的处理程序来覆盖默认响应,信号来临时就会执行用户定义的行为,而不是信号的默认行为
    • 线程池

      线程池thread pool)的主要思想是在进程开始的时候创建一定数量的线程,并放入到池中以等待工作。当服务器收到请求时,它会唤醒池中的一个线程(如果有可用线程的话),并将请求交由该线程处理。处理完 请求之后,再让线程返回池中等待下一个任务。如果池中没有可用线程,则服务器会一直等待,直到有空线程为止

    • 线程特定数据

      线程特定数据(thread-specific data),也就是线程局部数据,为每一个线程所私有而不与其他线程共享。

    • 调度程序激活

      调度程序激活机制 实际上是一种内核与线程库进行通信的机制。目的是 模拟内核线程的功能,但是同时又保留线程库在用户空间中实现拥有更佳的性能以及灵活性的优点

      • 轻量级进程(Light-weight process,LWP)

        轻量级进程是在内核线程与用户线程之间设置的一个中间的数据结构。对于线程库,LWP表现为一种虚拟处理器,线程库可以调度用户线程运行在这些虚拟处理器上。(也就是说线程库在调度用户线程的时候,就把LPW当做处理器来看待,用户线程要运行就必须竞争LPW的使用权,而线程库调度就是负责分派LPW的使用权给适当的用户线程)

        线程_第5张图片
        轻量级进程(LWP)
      • 上行调用(upcall)

        上行调用upcall)可以认为是内核给用户空间的进程的运行时系统发送的一个信号,进程的运行时系统根据收到的信号对应的作出处理。

      • 调度程序激活机制
        • 每个内核线程会关联一个LWP,应用程序可以调度一个用户线程到一个可用的LWP上执行,而内核线程则会被调度到真实的物理处理器上执行。

        • 内核通过upcall告知应用程序一些特定的事件,比如一个用户线程A运行在LWP①上,它调用了一个阻塞的系统调用,那么内核将会发一个 upcallA 告知应用程序,让其阻塞并标识这个用户线程A。接着内核会分配一个新的LWP(记作LWP②)给应用程序,应用程序会在LWP②上处理刚才的upcallA,标记用户线程A为阻塞,解除其与LWP① 的关联,然后在应用程序通过调度,在就绪队列里面找到一个等待执行的 用户线程B,将其与LWP②关联,让其运行在LWP② 上。当 用户线程A 所等待的阻塞事件结束的时候,内核会发送另一个 upcallB,通知应用程序先前阻塞的 用户线程A 又可以运行了,然后应用程序根据情况进行调度(选择是否为其分配一个LWP让其执行,或是将其放到就绪队里当中)。

          线程_第6张图片
          调度程序激活流程

你可能感兴趣的:(线程)