1 | func main() { |
执行上面代码,是执行第一个go func的无限循环,还是打印”hello go!”?
由于 goroutine,Go 可以轻松地同时运行数万个。在最初的计算机时代,只有进程,进程的缺点更加明显,它们太重了。fork一个40M内存的进程,消耗了更多的资源。后来,出现了线程。初始化一个线程应该比进程少一个数据量级的内存,进程要轻得多。与进程相比,线程并发要好很多,但线程切换也得落入内核.如果频繁切换,这个开销还是比较大的。cpu要保存临时存储线程当前的运行状态等等。如果线程很多,CPU在上下文切换上浪费了太多时间。
后来出现了协程,用户协作。相对于线程的优势在于一个线程可以关联多个协程,避免创建过多线程,并且在一个协程执行完后,可以通知下一个协程执行,这样切换就不需要切换到内核模式。它可以在用户模式下完成。
goroutine的起源也来自协程,协程自然拥有协程的所有好处。当然,还有比传统协程更好的地方。创建一个goroutine只需要2k内存,线程切换会消耗大约1000-1500ns,也就是1ns。可以执行12-18条指令。goroutine切换只需要200ns左右,相比线程至少可以节省5倍的时间。一个操作系统级线程可以绑定到多个 goroutine。当发生goroutine切换时,只需要移除当前正在执行的goroutine,不需要进行线程切换。(goroutine不会直接占用cpu,执行超过一段时间就会放弃)
三者的关系:当M有P,P有G时,M才能执行G。
调度算法:
goroutine的优点是每个g不直接占用m
每个 M 初始化时,都会注册一个可以接收 sigurg 信号的处理程序。此 sigurg 信号由 sysmon 监视器发送。sysmon 占用单个 M,sysmon 会检查 goroutine 是否执行超过 10ms 或者是否执行了 GC(STW)。如果条件满足,sysmon会给对应的M发送sigurg信号,对应的handler开始执行,标记正在执行的G,然后判断当前栈是否溢出(morestack)。满足条件后,M会保存当前G的上下文(如果下次这个G还能被这个M执行时,可以通过上下文快速返回到上次执行位置),当前G会被丢弃进入全局 G 队列,而 M 继续执行下一个 G 。
如何执行文章开头代码:
在程序开头设置p=1,那么就没有并行问题了,只有一个goroutine可以同时执行,要么是go func的无限循环,要么是main goroutine的time.sleep以后。
PS: go1.14以后,goroutine支持信号抢占式调度