go语言一个大的语言特色就是goroutine协程,而和很多同事沟通的时候,他们都认为goroutine很快,今天我们就来看一看goroutine是如何运行的。
MPG模型
go使用的是MPG模型,意思是通过一个全局的调度器来实现goroutine协程的调度,来达到通过分配平均使用CPU资源。
go的调度器有3个重要的结构,M(OS线程)、P(协程调度器),G(goroutine协程)
- M(OS线程):是操作系统的线程,一个程序可以模拟出多个线程。
- P(逻辑处理器or协程调度器):这个一个专门调度goroutine协程的逻辑处理器,或者称为协程调度器都可以。
- G(goroutine协程):goroutine协程。
用户空间线程和内核空间线程之间的映射关系有:N:1、1:1和M:N
- N:1,多个(N)用户线程始终在一个内核线程上跑,context上下文切换确实很快,但是无法真正的利用多核。
- 1:1,一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文switch很慢。
- M:N,多个goroutine在多个内核线程上跑,这个看似可以集齐上面两者的优势,但是无疑增加了调度的难度。
而go使用的就是M:N这种映射关系。
实现原理
当go启动一个进程的时候,会默认创建一个线程,这个线程会有一个逻辑处理器,通过具体的逻辑处理器处理goroutine协程。如下图所示:
我们假设当前线程为M2,逻辑处理器为P0,有4个协程G1、G2、G3、G4等待执行,当前正在执行G1
假如当G1有执行文件阻塞的操作,这个时候逻辑处理器P0会将G1分离处理,同时与线程M2分离,如下图:
这个时候原有的逻辑处理器P0下面还有很多协程需要执行,于是会生成一个新的线程M3来继续进行当前逻辑处理器P0的处理,此时顺序执行G2。而原有的线程M2和G1则等待文件阻塞操作的完成。
此时M3正常执行goroutine,过了一段时间,原有的G1阻塞操作完成,等待被继续执行,而此时G2也执行完成,G1会被重新分配回到逻辑处理器P0进行执行。
而这个时候,线程M2并不会立即销毁,而是等待被下次利用。这样一个简单的goroutine协程调度流程就完成了。下面来一张完整图:
但是这个只是针对系统文件的I/O操作的情况。如果是针对网络I/O的情况,稍微有一点不一样。
涉及网络I/O操作的时候,会使用网络轮询器来进行操作,对这个不太了解,有想深入了解的同学可以查阅相关资料。
但是原理是一样的,都是通过将阻塞的G放到其他线程处理,也放一张完整图:
至此,一个单一的MPG模型就完成了,但是如果实现上面的M:N模型乃?
那就是多线程操作了。上面的列子只是说明一个单一的线程在执行goroutie调度。go启动的时候默认是启动4个线程M,4个线程M都都是一个MPG。图示如下:
当我们要分配很多goroutine协程的时候,会被平均分配到各个线程M上,这样就实现了并行处理操作。
并发与并行
从上面的简单原理我们可以看到,go采用的是线程+协程的模式,并不是单一的协程。
- 并发:同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。
- 并行:同时做很多事情。
协程实现的是并发,线程实现的是并行。将并发和并行的结合,会做到全程的高性能。
小结
无论go语言如何变种,都逃不过操作系统的进程、线程。而我们所谓的go协程很快,是因为实现了一个复杂的调度器,来同时使用线程和goroutine协程,这样即可以充分利用多核CPU资源。
当然,协程的一个优点是开销非常小,要相对于大规模的线程,协程开销的确非常小。
每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少(goroutine:2KB ,线程:8MB)
所以这也是其中一个重要原因。
在上一篇我们了解了一下go的goroutine的大致实现原理,知道go为了实现高性能,采用了线程和协程的MPG模型,下面我们就通过具体的代码示例简单验证一下。
首先,我们给下面的程序只分配一个逻辑处理器,即相当于只开有一个线程
import (
"fmt"
"runtime"
"sync"
)
func main() {
// 分配一个逻辑处理器给调度器使用
runtime.GOMAXPROCS(1)
// wg用来等待程序完成
// 计数加2,表示要等待两个goroutine
var wg sync.WaitGroup
wg.Add(2)
fmt.Println("开始执行goruntine\n")
// goroutine运行打印大写字母3次
go printUpperChar(&wg)
// goroutine运行打印小写字母3次
go printLowerChar(&wg)
// 等待goroutine结束
fmt.Println("等待goroutine执行结束\n")
wg.Wait()
fmt.Println("程序终止")
}
//printLowerChar 打印小写字母表3次
func printLowerChar(wg *sync.WaitGroup) {
// 在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done()
// 显示字母表3次
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c ", char)
}
}
fmt.Println("\n")
}
//printUpperChar 打印大写字母表3次
func printUpperChar(wg *sync.WaitGroup) {
// 在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done()
// 显示字母表3次
for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c ", char)
}
}
fmt.Println("\n")
}
执行结果:
开始执行goruntine
等待goroutine执行结束
a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
程序终止
我们可以看到,执行结果是协程执行的两个方法,都是一个方法执行完了,才会有另外一个方法执行输出。无论我们执行多少次都是这样。
这是因为我们就只有一个调度器P,只会由一个线程用到一个CPU资源。
下面我们将调度器修改为2个,这个时候go会自动分配2个CPU资源。
// 其他没有变化,只是将runtime.GOMAXPROCS(2)中的1修改为了2
func main() {
// 分配一个逻辑处理器给调度器使用
runtime.GOMAXPROCS(2)
// wg用来等待程序完成
// 计数加2,表示要等待两个goroutine
var wg sync.WaitGroup
wg.Add(2)
fmt.Println("开始执行goruntine\n")
// goroutine运行打印大写字母3次
go printUpperChar(&wg)
// goroutine运行打印小写字母3次
go printLowerChar(&wg)
// 等待goroutine结束
fmt.Println("等待goroutine执行结束\n")
wg.Wait()
fmt.Println("程序终止")
}
执行结果:
css
复制代码开始执行goruntine
等待goroutine执行结束
a b c d e f g A B C D E F G H I J K L M N O h i j k l m n o p q r s t u P Q R S v w x y z a b c d e f g h i j T U V W k l m n o p q r s t u v w x y z a b X Y Z A B C D E F G H I J K L M N O P Q R S T U V c d e f W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
g h i j k l m n o p q r s t u v w x y z
程序终止
这次我们发现,输出不再是之前的一个方法执行完了接着执行另外一个方法,而是乱序的了,大小写夹杂着输出。着就是多个线程在同时执行,就是所谓了并行。
不用说,并行的效率肯定会更高效。那我们有必要在每次都指定runtime.GOMAXPROCS(1)
的个数吗?
答案是不用,因为go默认会启动4个逻辑处理器,但是不知道后续版本中是会根据CPU核数启动,还是指定的4个,这个需要查阅相关文档。
如果我们编写了一个应用程序,需要将性能最大化,可以进行一定的修改调整,然后进行性能测试。
作者:y80x86ol
链接:https://juejin.cn/post/6987361342688591903
作者:y80x86ol
链接:https://juejin.cn/post/6987360989150707720