进程 和 系统级线程 以及 用户级线程(主要讲协程)

139 阅读5分钟

执行单元

执行单元是指 CPU 调度和分派的基本单位,它是一个 CPU 能正常运行的基本单元。执行单元是可以停下来的,只要能把 CPU 状态(其实就是寄存器的值)全部保存起来,等到这个执行单元再被调度的时候,就把状态恢复过来就行了。我们把这种保存状态,挂起,恢复执行,恢复状态的完整过程,称为执行单元的调度 (Scheduling)。

进程(资源分配的单位)

  • 出现前的情况
    以前只有单核计算机,运行的是实时系统,只能串行处理任务。面对I/O密集型的任务,只能长时间等待

image.png

  • 解决方案
    为了实现多任务处理(说白了,就是面对多个程序的执行,通过减少阻塞、异步执行达到提高效率的目的),提出了分时和调度,因此出现了时间片和进程。
  • 特点
    进程可以独占计算资源(CPU)、内存资源(独立的内存空间和页表)和文件资源(文件表)。当运行一个可执行程序的时候,操作系统就会启动一个进程。进程会被操作系统管理和调度,被调度到的进程就可以独占 CPU 了。CPU 就像是一个可以轮流使用的工作台,多个进程可以在工作台上工作,时间到了就会带着自己的工作离开工作台,换下一个进程上来工作。

线程(执行实体)

系统级线程

  • 出现前的情况
    一个应用为了使内部逻辑并发执行,被拆分成粒度更小的多个任务进程时,它所占用的资源就会比较多,切换时带来的时间和资源开销也更大。

image.png

  • 解决方案 为了减少切换进程带来的开销,提出了开销更小的线程。(说白了,就是针对程序内部运行逻辑的执行,通过减少程序内部逻辑的阻塞、异步执行来提高内部执行效率)
  • 特点 同一个进程中的线程独占计算资源(CPU),但共享该进程的内存空间,文件表,文件描述符等资源,它与同一个进程的其他线程共享资源分配。但每个线程也有自己的私有空间,这就是线程的栈。线程在执行函数调用的时候,会在自己的线程栈里创建函数栈帧。

用户级线程

协程

  • 出现前的情况 刚有线程概念时,线程只能在用户态中执行,因此线程的调度由用户态进程中的调度器执行,对于内核来说调度的仍是进程,因此如果一个进程中的某一个线程发生阻塞,那么整个进程中的线程都无法执行。后来线程的技术逐步成熟,则在内核中添加了内核级线程,此时CPU调度的最小单位就变为线程。
    有了线程就可以高并发,但还需要优化。随着应用越来越复杂,进程要处理的任务也更多了,为大量任务而创建大量线程带来了内存开销、内核态和用户态之间的切换会带来的计算开销

image.png

  • 解决方案 减少内核态和用户态之间的切换,提出了类似协程这种用户级线程,内核线程依然叫 “线程 (thread)”,用户线程叫 “协程 (co-routine)”。一个 “用户态线程” 必须要绑定一个 “内核态线程”,但是 CPU 并不知道有 “用户态线程” 的存在,它只知道它运行的是一个 “内核态线程”(Linux 的 PCB 进程控制块)。
    • 概念
      协程是比线程更轻量的执行单元(在 C++ 中使用各种协程库,或者在 Lua、Go 等语言中使用原生协程)。
      进程和线程的调度是由操作系统负责的,而协程则是由执行单元相互协商进行调度的,所以用户级线程切换的实现是在用户态直接切换线程的栈和寄存器。
      例如,可以在用户态下实现任务调度以减少阻塞,提高协程利用率。在同一个线程中,只有前一个协程主动地执行 yield 函数,让出 CPU 的使用权,下一个协程才能得到调度。因为程序自己负责协程的调度,所以大多数时候,我们可以让不那么忙的协程少参与调度,从而提升整个程序的吞吐量,而不是像进程那样,没有繁重任务的进程,也有可能被换进来执行。
      协程的切换和调度所耗费的资源是最少的,Go 语言把协程调度和 I/O 多路复用的思想结合在一起,提供了非常便捷的 I/O 接口,使得协程的概念深入人心。
      协程可以分配更小的栈空间,例如 GO 语言中,g0 上的栈是系统分配的栈,在linux上栈大小默认固定8MB,不能扩展,也不能缩小,而普通g一开始只有2KB大小,可扩展。
  • 特点
    • 占用的资源更少,内存按需分配
    • 所有的切换和调度都发生在用户态
    • 调度是协商式的(当然也有抢占式,如 G 分配的时间片是10ms,超时后会被 g0 挂起),阻塞时可以让出线程