在并发编程中,常见的并发调度模型是多进程、多线程,而Go中则实现了协程的并发调度大大提高了并发性能。

协程与进程、线程:

  • 进程:

    • 进程是系统进行资源分配的最小单位,是程序运行的基本单位
    • 每个进程都有独立的内存空间,不同进程之间不共享也不抢占资源
    • 进程由程序、数据集合、进程控制块(PCB)三部分组成
    • 由于进程占用的资源多,上下文切换时开销大,但是因为独享资源,所以安全稳定
  • 线程

    • 线程是CPU调度的最小单位,是进程的运行实体
    • 多个线程共享一个进程的资源,同时也有属于自己的线程级资源
    • 由线程控制块(TCB)进行控制和管理
    • 线程资源比进程少,上下文切换开销更小
  • 协程

    • 是用户态的调度单位,比线程更轻量级
    • 协程由用户程序调度不涉及用户态与内核态的切换,开销更小

什么是上下文切换?

因为CPU是时间片轮转调度的,所以CPU要知道每个任务从哪里加载,从哪里开始运行,也就是CPU寄存器和程序计数器,这就是CPU 上下文,而每个任务都有对应的CPU上下文,切换不同的任务执行的时候,就需要切换CPU 上下文。

Go中的协程:

goroutine是go中实现的用户级并发调度单元,也被称为轻量级线程

线程、goroutine的对比:

线程协程
内存占用线程栈内存消耗为1MB协程栈大小为2KB,但是可以自动扩容
创建和销毁会导致用户态和内核态的切换,解决办法是线程池只需要在用户态执行,创建和销毁的消耗小
切换需要保存各种寄存器只需保存3个寄存器

goroutine的结构:

type g struct {
    goid    int64 // 唯一的goroutine的ID
  sched   gobuf //goroutine切换时,保存g的上下文
    stack   stack //栈
    gopc //调用goroutine的地址
  startpc //调用goroutine的入口
  ...
}

type gobuf struct{
    sp uintptr //栈指针的位置
  pc uintptr //运行到程序位置
  g  guintptr //指向goroutine
  ret uintptr //保存系统调用的返回值
  ...
}

type stack struct {
    lo uintptr //栈的下界地址
  hi uintptr // 栈的上界地址
}

goroutine的状态:

  1. 空闲中:_Gidle【G刚刚创建,仍未初始化】
  2. 待运行:_Grunable 【就绪状态,G在运行队列中,等待M取出来并运行】
  3. 运行中:_Grunning【M在运行G,此时M拥有一个P】
  4. 系统调用中:_Gsyscall【M正在运行的G发起系统调用,此时M不拥有P】
  5. 等待中:_Gwaiting【G被挂起,在等待某些条件完成,此时G不在运行也不在运行队列中(可能在channel的等待队列中)】
  6. 已终止:_Gdead【G未被使用,也可能执行完毕】
  7. 栈复制中:_Gcopystack【G正在获取一个新的栈空间,并把原来的内容复制过去(用于防止GC扫描)】

goroutine状态流转:

943E311D-9879-49BF-B7AD-25581B61D604.png

创建:通过go关键的创建goroutine会新建自己的栈空间,同时在sched中维护栈地址和程序计数器这些信息,G被创建好之后,会优先放入本地队列中,本地队列满了之后,回放在全局队列中

运行:goroutine只是一个数据结构,让goroutine运行起来的是调度器,也就是GMP模型

阻塞:channel的读写操作、等待锁、等待网络数据、系统调用都可能发生阻塞,通过执行runtime.gopark() 让出CPU时间片,让调度器安排其他等待的任务运行

唤醒:处理_Gwating 的G,在调用runtime.goready()之后,会被唤醒,唤醒之后毁被重新放在M对应的P的本地队列中等待执行

退出:G执行完之后,会调用底层函数runtime.Goexit(),当调用该函数之后,goroutine会设置成dead状态

标签: none

添加新评论