[Golang 修仙之路] Go语言篇:进程、线程、协程

52 阅读3分钟

我们先从定义的角度, 来对比这三个概念

进程

进程是操作系统资源分配的基本单位. 分配给进程的资源包括「内存」、「打开的文件」、「设备」等. 这些资源由「进程控制块PCB」记录, PCB中还存储着进程的唯一标识「PID」.

运行中的程序才叫「进程」, 高级语言程序通过编译生成可执行的「二进制文件」, 可执行文件被执行后. 进程进入运行状态, 这时程序才变为进程. 指令被调入内存, 存放在我们刚才说的代码段. CPU不断从代码段中取指令, 译码, 执行指令循环, 程序就运行起来了.

线程

  • 线程共享进程的「内存」、「打开文件」,但是线程拥有自己的「寄存器」、「栈」。

  • 线程是CPU调度的基本单位。

  • 线程分为「用户线程」和「内核线程」,最终要占用CPU执行的,必须是「内核线程」。

  • 「线程调度器」是内核里的一个功能模块,负责挑选哪个内核线程上 CPU。

  • 用户线程只是一个运行逻辑的抽象,不能直接操作硬件(个人认为,这里的硬件主要指CPU资源)。

  • 这样一来,我们所说的「一对一」,「多对一」,「多对多」模型,就好理解了 -- 用户线程需要绑定到内核线程上运行,因为内核线程能占用CPU。Go的协程调度机制GMP模型,就是典型的「多对多」模型。

  • 「PCB」中包含多个「TCB」,二者都是操作系统在「内核空间」中维护的一块数据结构。

线程上下文切换

  • 线程上下文切换是指CPU 停止执行当前线程,把执行权转交给另一个线程的过程。

  • 上下文(Context) :包括 CPU 寄存器的值、程序计数器(PC)、栈指针(SP)、线程状态等信息。

  • 我自己的一个理解:

    • 用户态 -- 就是用户自己写的代码,代码被翻译成指令,需要CPU执行。

    • 内核态 -- 就是OS官方的代码,能正确的操作硬件资源,要让我来写肯定完犊子了。

个人的一些理解:

  • CPU正在执行用户的代码,一条一条的读指令,突然发现有一个「中断」。
  • 「中断」很复杂,用户自己的代码处理不来,就要用OS官方写的代码,于是陷入「内核态」。
  • 得知要进行进程切换,内核中负责进程切换的代码先「保存现场」,即保存「上下文」到「TCB」中。
  • 内核的「线程调度模块」选择要执行的线程,为线程B。
  • 内核的代码,把线程B的「TCB」中的寄存器信息,栈指针,加载到CPU的对应寄存器中。
  • CPU的寄存器处于新的正确的位置了,又可以执行用户代码了,于是从内核态切换回用户态。

协程

我个人这样理解,goroutine是协程的一个实现。

goroutine 和 线程 的区别:

  • 协程在用户空间模拟了一个类似TCB的结构体,拥有自己的PC和栈指针。
  • 协程占用「内存」更小,一个协程初始拥有2KB的栈空间,可以扩容。一个线程拥有MB级别的栈空间。
  • 协程的「上下文切换开销」更小,完全由Go runtime管理,不需要陷入内核。
  • 协程的「创建与销毁开销」更小,进程创建与销毁也需要陷入内核,协程还是由Go runtime 管理。
type g struct {
    stack       stack      // 栈信息
    sched       gobuf      // 寄存器等上下文
    ...
}

type gobuf struct {
    sp   uintptr  // 栈指针
    pc   uintptr  // 程序计数器
    g    guintptr // 关联的 g
}