Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

Vinllen Chen


烛昭

go中的协程与线程的区别

  对用户来说,协程与线程几乎没什么区别,但是实际上还是有一些区别的。
  说明:本文对协程和goroutine,OS线程和内核线程都是一个概念,未加区分。

1. 栈大小区别

  我们知道,线程是有固定的栈的,基本都是2MB,当然,不同系统可能大小不太一样,但是的确都是固定分配的。这个栈用于保存局部变量,用于在函数切换时使用。但是对于goroutine这种轻量级的协程来说,一个大小固定的栈可能会导致资源浪费:比如一个协程里面只print了一个语句,那么栈基本没怎么用;当然,也有可能嵌套调用很深,那么可能也不够用。
  所以go采用了动态扩张收缩的策略:初始化为2KB,最大可扩张到1GB。

2. goroutine没有id

  每个线程都有一个id,这个在线程创建时就会返回,所以可以很方便的通过id操作某个线程。但是在goroutine内没有这个概念,这个是go语言设计之初考虑的,防止被滥用,所以你不能在一个协程中杀死另外一个协程,编码时需要考虑到协程什么时候创建,什么时候释放。

3. GOMAXPROCS

  GOMAXPROCS用于设置上下文个数,这个上下文用于协程间的切换,默认值是CPU的个数,也就是说这个个数是指定同时执行协程的OS线程数(此部分看不懂没关系,看完第4节应该就懂了)。这么说可能抽象一点,《The Go Programming Language》这本书里举了个例子:

for {  
    go fmt.Print(0)
    fmt.Print(1)
}

$ GOMAXPROCS=1 go run example.go
11111111111111111100000000000000000000111111111...  
$ GOMAXPROCS=2 go run example.go
01010101010010101001110010101010010101010101010...  

  第一次执行语句指定只启动一个上下文,那么由于是多对1映射到1个OS线程,那么1次只能跑一个协程,所以会跑一段时间再进行切换(由调度器进行判断什么时候切换,而不是内核)。第二次启动二个上下文,映射到2个OS线程,那么同一时间有2个干活的OS线程,所以能看到0和1交替打印,也就是说,此时真正实现了并发。

4. goroutine调度

  这部分可以写很多,但总的来说就是,协程和线程的区别在于:线程切换需要陷入内核,然后进行上下文切换,而协程在用户态由协程调度器完成,不需要陷入内核,这代价就小了;另外,协程的切换时间点是由调度器决定的,而不是系统内核决定的,尽管他们切换点都是时间片超过一定阈值,或者进入I/O或睡眠等状态;再次,还有垃圾回收的考虑,因为go实现了垃圾回收,而垃圾回收的必要条件时内存位于一致状态,这就需要暂停所有的线程,如果交给系统去做,那么会暂停所有的线程使其一致,而在go里面调度器知道什么时候内存位于一致状态,那么就没有必要暂停所有运行的协程。
  以下介绍一下go协程大体的调度过程,参考go scheduler这篇博客。
  对线程来说,有三种映射(用户线程与内核线程的因素)模型:

  • 一对一模型(1:1)。一个用户线程映射到一个内核线程,用户线程在存活期都会绑定到一个内核线程,一旦退出,2个线程都会退出。优点是实现了真正的并发,多个线程同时跑在不同的CPU上;缺点是,如果用户线程起多了,内核线程肯定不够用,那么就需要切换,涉及到上下文的切换,代价比较大。
  • 多对一模型(M:1)。多个用户线程映射到一个内核线程。优点是,多个用户线程切换比较快,不需要内核线程上下文切换;缺点是,如果一个线程阻塞了,那么映射到同一个内核线程的用户线程将都无法运行。
  • 多对多模型(M:N)。综合以上两种模型,go采用的就是这种。下面进行具体介绍。

  go调度里面有三个角色:三角形M代表内核线程,正方形P代表上下文,圆形G代表协程: goroutine1
下面图我们看到他们之间的对应规则:一个M对应一个P,一个P下面挂多个G,但一个时候只有一个G在跑,其余都是放入等待队列,等待下一次切换时使用。
goroutine2
  那么假如一个运行的协程G调用syscall进入阻塞怎么办?如下图左边,G0进入阻塞,那么P会转移到另外一个内核线程M1(此时还是1对1)。当syscall返回后,需要抢占一个P继续执行,如果抢占不到,G0挂入全局就绪队列runqueue,等待下次调度,理论上会被挂入到一个具体P下面的就绪队列runqueu(区别于全局runqueue)。
goroutine3
  假如一个P0下面的所有G都跑完了,怎么办?这时候会从别的P1下面就绪队列抢占G进行运行,个数为P1就绪队列的一半。
goroutine4

总结

  具体调度细节这块可以参考go-scheduler进行查看,或者可以看《go并发编程实战》这本书,4.1章节讲的还凑合。

说明:

转载请注明链接:http://vinllen.com/gozhong-de-xie-cheng-goroutineyu-xian-cheng-de-qu-bie/

参考:

http://morsmachine.dk/go-scheduler
The Go Programming Language
https://www.jianshu.com/p/5a4fc2729c17
《go并发编程实战》4.1章节


About the author

vinllen chen

Beijing, China

格物致知


Discussions

comments powered by Disqus