Goroutine和进程、线程、协程
进程
- 内核进行调度,有CPU时间片的概念,进行抢占式调度(有多种调度算法)
线程
- 内核进行调度(内核级线程),有CPU时间片的概念,进行抢占式调度(有多种调度算法)
协程
- 用户级线程,对内核是透明的,也就是系统并不知道有协程的存在,是完全由用户的程序自己调度的;因为是由用户程序自己控制,那么就很难像抢占式 调度那样做到强制的CPU控制权切换到其他进程/线程,通常只能进行协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到
- 用户自己调度
goroutine
-
goroutine就是协程,不同的是,Golang在runtime、系统调用等多方面对goroutine调度进行了封装和处理,当遇到长时间执行或者进行系统调用时, 会主动把当前goroutine的CPU (P) 转让出去,让其他goroutine能被调度并执行,也就是Golang从语言层面支持了协程
-
go语言帮助用户进行调度
-
不要通过共享内存通信,相反,通过通信共享内存
-
内存消耗方面
每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。 goroutine: 2KB 线程: 8MB
-
线程/goroutine 切换(调度)开销方面
线程/goroutine 切换开销方面,goroutine 远比线程小 线程: 涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP…等寄存器的刷新等。 goroutine: 只有三个寄存器的值修改 - PC/SP/DX
goroutine和线程区别
从调度上看,goroutine的调度开销远远小于线程调度开销。
OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个调度器内核函数。这个函数暂停当前正在运行的线程,把他的寄存器信息保存到内存中,查看线程列表并决定接下来运行哪一个线程,再从内存中恢复线程的注册表信息,最后继续执行选中的线程。这种线程切换需要一个完整的上下文切换:即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器的数据结构。某种意义上,这种操作还是很慢的。
Go运行的时候包涵一个自己的调度器,这个调度器使用一个称为一个M:N调度技术,m个goroutine到n个os线程(可以用GOMAXPROCS来控制n的数量),Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发的,他不需要切换到内核语境,所以调度一个goroutine比调度一个线程的成本低很多。
从栈空间上,goroutine的栈空间更加动态灵活。
每个OS的线程都有一个固定大小的栈内存,通常是2MB,栈内存用于保存在其他函数调用期间哪些正在执行或者临时暂停的函数的局部变量。这个固定的栈大小,如果对于goroutine来说,可能是一种巨大的浪费。作为对比goroutine在生命周期开始只有一个很小的栈,典型情况是2KB, 在go程序中,一次创建十万左右的goroutine也不罕见(2KB*100,000=200MB)。而且goroutine的栈不是固定大小,它可以按需增大和缩小,最大限制可以到1GB。
goroutine没有一个特定的标识。
在大部分支持多线程的操作系统和编程语言中,线程有一个独特的标识,通常是一个整数或者指针,这个特性可以让我们构建一个线程的局部存储,本质是一个全局的map,以线程的标识作为键,这样每个线程可以独立使用这个map存储和获取值,不受其他线程干扰。
goroutine中没有可供程序员访问的标识,原因是一种纯函数的理念,不希望滥用线程局部存储导致一个不健康的超距作用,即函数的行为不仅取决于它的参数,还取决于运行它的线程标识。