Go语言中Goroutine与线程的区别
1、什么是Goroutine?
Goroutine是建立在线程之上的轻量级的抽象。它允许我们以非常低的代价在同一个地址空间中并行地执行多个函数或者方法。相比于线程,它的创建和销毁的代价要小很多,并且它的调度是独立于线程的。
package main import ( "fmt" "time" ) func learning() { fmt.Println("My first goroutine") } func main() { go learning() time.Sleep(1 * time.Second) fmt.Println("main function") }
这段代码的输出是:
My first goroutine
main function
如果将Sleep去掉,将会输出的是:
main function
这是因为,和线程一样,golang的主函数(其实也是跑在一个goroutine中)并不会等待其他goroutine结束。如果主goroutine结束了,所有其他goroutine都将结束。
2、Goroutine与线程的区别
许多人认为goroutine比线程运行得更快,这是一个误解。Goroutine并不会更快,它只是增加了更多的并发性。当一个goroutine被阻塞(比如等待IO),golang的scheduler会调度其他可以执行的goroutine运行。与线程相比,它有以下的几个优点:
内存消耗更少:
Goroutine所需要的内存通常只有2kb,而线程则需要1Mb(500倍)
创建与销毁的开销更小:
由于线程创建时需要向操作系统申请资源,并且在销毁时将资源归还,因此它的创建和销毁的开销比较大。相比之下,goroutine的创建和销毁是由go语言在运行时自己管理的,因此开销更低。
切换开销更小:
只是goroutine之于线程的主要区别,也是golang能够实现高并发的主要原因。线程的调度方式是抢占式的,如果一个线程的执行时间超过了分配给它的时间片,就会被其他可执行的线程抢占。在线程切换的过程中需要保存/恢复所有的寄存器信息,比如16个通用寄存器,PC(Program Counter)、SP(Stack Pointer)段寄存器等等。而goroutine的调度是协同式的,它不会直接地与操作系统内核打交道。当goroutine进行切换的时候,之后很少量的寄存器需要保存和恢复(PC和SP)。因此goroutine的切换效率更高。
3、Goroutine的调度
goroutine的调度方式是协同式的,在协同式调度中,没有时间片的概念。为了并行执行goroutine,调度器会在以下几个时间点对其进行切换:
- Channel接收或者发送会造成阻塞的消息
- 当一个新的goroutine被创建时
- 可以造成阻塞的系统调用,如文件和网络操作
- 垃圾回收
调度器具体是如何工作的呢,Golang调度器中有三个概念:
- Processor(P)
- OSThread(M)
- Goroutines(G)
在一个Go程序中,可用的线程数是通过GOMAXPROCS来设置的,默认值是可用的CPU核数。我们可以用runtime包来动态改变这个值。OSThread调度在processor上,goroutines调度在OSThreads上。
Golang的调度器可以利用多processor资源,在任意时刻,M个goroutine需要被调度到N个OS threads上,同时这些threads运行在至多GOMAXPROCS个processor上(N <= GOMAXPROCS)。Go scheduler将可运行的goroutines分配到多个运行在一个或多个processor上的OS threads上。
每个processor有一个本地goroutine队列。同时有一个全局的goroutine队列。每个OSThread都会被分配给一个processor。最多只能有GOMAXPROCS个processor,每个processor同时只能执行一个OSThread。Scheculer可以根据需要创建OSThread。
在每一轮调度中,scheduler找到一个可以运行的goroutine并执行直到其被阻塞。由此可见,操作系统的一个线程下可以并发执行上千个goroutine,每个goroutine所占用的资源和切换开销都很小,因此,goroutine是golang适合高并发场景的重要原因。