Go 协程实现原理

1、进程、线程是什么

在这里插入图片描述

在这里插入图片描述

多个线程可以属于同一个进程并共享内存空间。因为多线程不需要创建新的虚拟内存空间,所以它们也不需要内存管理单元处理上下文的切换,线程之间的通信也正是基于共享的内存进行的,与重量级的进程相比,线程显得比较轻量。

虽然线程比较轻量,但是在调度时也有比较大的额外开销。每个线程会都占用 1M 以上的内存空间,在切换线程时不止会消耗较多的内存,恢复寄存器中的内容还需要向操作系统申请或者销毁资源,每一次线程上下文的切换都需要消耗 ~1us 左右的时间1,但是 Go 调度器对 Goroutine 的上下文切换约为 ~0.2us,减少了 80% 的额外开销

2、协程是怎么实现的

goroutine是由Go运行时管理的用户层轻量级线程。相较于操作系统线程,goroutine的资源占用和使用代价都要小得多。我们可以创建几十个、几百个甚至成千上万个goroutine。

将这些goroutine按照一定算法放到CPU上执行的程序就称为goroutine调度器(goroutine scheduler)。一个Go程序对于操作系统来说只是一个用户层程序,操作系统眼中只有线程,goroutine的调度全要靠Go自己完成。

G-P-M模型

在这里插入图片描述
在这里插入图片描述

关于G、P、M的定义,可以参见$GOROOT/src/runtime/runtime2.go这个源文件。

  • G — 表示 Goroutine,它是一个待执行的任务;

  • M — 表示操作系统的线程,它由操作系统的调度器调度和管理;
    Go 语言并发模型中的 M 是操作系统线程。调度器最多可以创建 10000 个线程,但是其中大多数的线程都不会执行用户代码(可能陷入系统调用),最多只会有 GOMAXPROCS 个活跃线程能够正常运行。

  • P — 表示处理器,它可以被看做运行在线程上的本地调度器;
    如何执行Goroutine:

    • 有一定几率从全局队列获取Goroutine
    • 从本地运行队列获取Goroutine
    • 从其他处理器窃取代运行的Goroutine
抢占式调度
最开始的时候:

不支持抢占式调度,这导致一旦某个G中出现死循环的代码逻辑,那么G将永久占用分配给它的P和M。

Go 1.2

抢占式调度的原理是:在每个函数或方法的入口加上一段额外的代码,让运行时有机会检查是否需要执行抢占调度。而是否需要被强占是有个监控线程做检查并设置标记的。

在Go程序启动时,运行时会启动一个名为sysmon的M(一般称为监控线程)

sysmon每20us~10ms启动一次。

// $GOROOT/src/runtime/proc.go

// forcePreemptNS是在一个G被抢占之前给它的时间片
const forcePreemptNS = 10 * 1000 * 1000 // 10ms

func retake(now int64) uint32 {
    ...
    // 抢占运行时间过长的G
    t := int64(_p_.schedtick)
    if int64(pd.schedtick) != t {
        pd.schedtick = uint32(t)
        pd.schedwhen = now
        continue
    }
    if pd.schedwhen+forcePreemptNS > now {
        continue
    }
    preemptone(_p_)
    ...
}
  1. 编译器会在调用函数前插入 runtime.morestack
  2. Go 语言运行时会在垃圾回收暂停程序、系统监控发现 Goroutine 运行超过 10ms 时发出抢占请求 StackPreempt
  3. 当发生函数调用时,可能会执行编译器插入的 runtime.morestack,它调用的 runtime.newstack 会检查 Goroutine 的 stackguard0 字段是否为 StackPreempt
  4. 如果 stackguard0StackPreempt,就会触发抢占让出当前线程;
Go 1.14

https://cloud.tencent.com/developer/article/1836265

channel阻塞或网络I/O情况下的调度

如果G被阻塞在某个channel操作或网络I/O操作上,那么G会被放置到某个等待队列中,而M会尝试运行P的下一个可运行的G。如果此时P没有可运行的G供M运行,那么M将解绑P,并进入挂起状态。

系统调用阻塞情况下的调度

在这里插入图片描述
在这里插入图片描述

如果G被阻塞在某个系统调用上,那么不仅G会阻塞,执行该G的M也会解绑P(实质是被sysmon抢走了),与G一起进入阻塞状态。如果此时有空闲的M,则P会与其绑定并继续执行其他G;如果没有空闲的M,但仍然有其他G要执行,那么就会创建一个新M(线程)。当系统调用返回后,阻塞在该系统调用上的G会尝试获取一个可用的P,如果有可用P,之前运行该G的M将绑定P继续运行G;如果没有可用的P,那么G与M之间的关联将解除,同时G会被标记为runnable,放入全局的运行队列中,等待调度器的再次调度。

小结

在这里插入图片描述

线程管理
协程的大小
为什么不需要协程池
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值