谈谈Golang的协程和传统线程之间的区别与协程调度

2020年10月9日 0 条评论 1.31k 次阅读 0 人点赞

先说说官话。

线程,有时被称为轻量级进程,是操作系统调度与CPU执行的最小单位

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

看到线程与协程这里你是否有点迷?其实最主要的是需要理解何为用户态、何为内核。用户态即为我们应用程序所存在的空间,而内核态存在于操作系统层。

简而言之,协程也就是我们应用程序模拟实现的线程罢了。

再来谈谈传统线程的缺点

我们的应用程序每创建一个线程,需要发起一次系统调用,这是一次用户态到内核态的切换,并且由于CPU调度的最小单位是线程,如果创建过多的线程会造成调度时CPU时间片白白的浪费。

而Golang的机智之处在于go程序都有一个环境变量作为虚拟核心数设置,即GOMAXPROCS,它默认设置的值与你CPU核心数相同。

因此,我们在Go程序中每创建的Goroutine其实都是基于GOMAXPROCS之上的。

例如:我的电脑是4核,当我们创建了100个Goroutine时,那么这100个协程其实也只是真正的运行在4个核心之上。而100个协程之间的调度是由用户态应用程序自行控制的,它大大减少了从用户态和内核态之间的不停切换,因此加快了效率.......良好的压榨了CPU。并且由于没有过多的创建线程,因此它减少了CPU在线程之间的调度造成的时间片浪费。

而 Golang 内部的协程调度模型之GMP调度模型也便成为了面试官常考项。

再来谈谈协程调度

首先是runtime

多个 goroutine 好比平行宇宙中的每个世界,而 chan 则是 一个穿梭机,方便你在平行世界穿梭,在 go 的空间内,runtime 是上帝,主宰一切。所有对操作系统 API 的调度,都会被 runtime 层拦截以便达到高效调度。

用户态协程调度机制

  • N:1 多个用户态协程运行在一个 OS 线程上 (这里上文就讲过)
  • 1:1 一个用户态协程对应一个 OS 线程
  • M:N 任意数量的用户态协程可以运行在任意数量的 OS 线程上

goroutine 模型(GMP调度模型)

  • G:Goroutine golang 协程
  • P:Context M 与 P 的中介(有的文章也叫它调度器,它是实现 M:N 模型的关键)
  • M:系统线程

且M 必须拿到 P 才能够对 G 进行调度

它大概长下面这个样子

Goroutine调度时机

Goroutine 在 system call 和 channel call 时都可能发生阻塞,当程序发生 system call,M 会发生阻塞,同时唤起(或创建)一个新的 M 继续执行该队列的剩余 G

当程序发起一个 channel call,程序可能会阻塞,但不会阻塞 M,G 的状态会设置为 waiting,M 继续执行其他的 G, 当 G 的调用完成,会有一个可用的 M 继续执行它。

Goroutine上下文切换

上面的也都是官话,一切操作都只是为了压榨 CPU,谁让它那么快,又出现多核。

出现阻塞,意味 CPU 在当前执行域没活干了,它在干等,换 go 而言,调度器要 goroutine 上下文切换

chan 读写出现阻塞时,runtime 会隐式地进行上下文切换,而在极个别情况下,需要程序员显式编码操作,如下所示
至于切换到哪个 goroutine,由调度器决定。但可以肯定的是对同一 chan 所相关的 goroutine 执行有序

最后来见证 Goroutine vs Python yield

编程语言都是基于操作系统,显然 goroutine 能做的,其它 语言也可以。
只不过,比之于 java,python 之类上古语言,go 做的更多而已。

因此 Golang的协程比Python的强大之处就是在于它的协程能利用多核。

本文参考:https://learnku.com/articles/35045

日常摘录,完。

兰陵美酒郁金香

大道至简 Simplicity is the ultimate form of sophistication.

文章评论(0)

你必须 登录 才能发表评论