golang-协程理解

总结一下go协程的理解,如有错误望请指正。

网上都说协程是一种轻量级线程,线程又是一种轻量级的进程。这话在语言层面看来是没有错的,但它们的实现是不同的。

线程是cpu资源调度的最小单位。协程不由cpu进行调度,由应用程序进行调度,也就是由go进行调度。在go中,协程的调度也有专门的调度器。但go的协程调度器的复杂程度比cpu的线程调度器是要低许多的。

计算机进程/线程的运行是抢占式的,操作系统负责分配cpu执行时间给各个线程,当时间到达后,当前线程必须暂停进入睡眠,等待后续获得cpu执行时间再度恢复执行;当然线程也可以主动放弃cpu,进入睡眠,这一操作大多由程序员来控制。

协程实现原理

下面说下go中协程的实现原理,这些是我个人所知道的,错误之处望指正。 go中协程的实现原理还是基于线程的,大致由一个数据队列和多个线程实现。将一个个协程的代码放入数据队列,由go内部的线程去数据队列中拉取协程进行执行,协程本质还是运行在线程上的。go明面上并没有提供任何创建启动一个线程的方法,只提供了创建运行协程的方法。当用户创建的协程越来越多,线程处理不完时,go自己就会创建新的线程来执行协程。可以看到go本质上也是多线程的运行方式。但它与传统的一个请求一个线程的方式又有不同,在go中,http服务接受一个请求是创建一个协程来为这个请求服务的,可以看到,在资源利用方法,一个请求一个协程比一个请求一个线程要好很多。这也是go对并发编程友好的原因。

协程的调度

前面说了,go中协程有自己的调度算法,

go调度由三部分组成:

系统线程,用户协程和核心处理器。

处理器用于关联协程和线程,用户协程由处理器在线程上进行运行。

处理器的数量不会超过runtime包设置的GOMAXPROCS的大小。这个参数默认值为系统cpu数量。

一个处理器可以与多个线程关联,但同一时刻只能与一个线程建立绑定关系。

每个处理器有一个本地的协程队列,里面存储的是要运行的协程。同时全局环境也有一个全局队列里面也存储的要运行的协程。当本地队列满了,放不下新来的协程,就会取一半的协程放入全局队列。

当一个线程中的协程发生IO阻塞时,就会发生调度,这个线程中的处理器就会与这个线程解绑,带着本地队列后面的协程与其它线程建立绑定关系,继续运行后续的协程。选择下一个运行的协程会有1/61的机率从全局队列获取协程执行。发生阻塞的线程只保留与发生IO阻塞调用的协程的关系,当IO调用完毕,线程会试图去获取处理器,用来运行协程,如果获取不到,就会把协程放入全局协程队列。等待其它线程调度运行,自己进入休眠,等待被唤醒。

go中协程的执行不会主动放弃cpu,当一个线程执行一个协程时,如果这个协程没有发生系统调用或者进入睡眠(什么是系统调用文末讲解),那么在这个线程的cpu执行时间内,这个协程是会一直占用这个线程的。其它协程是得不到这个线程的执行权的。那什么时候协程会放弃cpu进入睡眠呢?就是在发生系统调用和主动进入睡眠时。这个时候这个协程后面的其它协程会由其它线程来进行调度执行,防止因为一个协程发生系统调用和主动进入睡眠时阻塞后面的协程执行。当协程系统调用完毕或睡眠结束后,重新进入数据队列等待其它线程进行调度执行它。

系统调用

在go中发生协程调度时的系统调用指的是各种计算机硬件调用,比如磁盘读取,键盘/鼠标事件读取这些系统IO调用,也就是说在发生IO操作时会发生协程调度,在go中发生网络IO也会发生协程调度,调用go标准库time进行睡眠也会发生协程调度,当channel阻塞的时候也会发生协程调度。

上面说了这么多,下面用一点代码看看效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"os"
"runtime"
)

func go1() {
for {
fmt.Print(1)
}
}
func go2() {
for {
fmt.Print(2)
os.Exit(1)
}
}
func main() {
runtime.GOMAXPROCS(2) // 设置最多有两个核心cpu,也就是两个处理器
go go1() // 启动一个协程
go go2() // 启动一个协程
for{}

}
上面的代码中启动了三个协程,一个主协程也就是main协程和两个普通协程,第一个协程打印1,第二个打印2,同时我们设置了最多两个处理器,其中主协程所在线程肯定占一个处理器,并且主协程不会释放处理器(for无限执行),另外一个处理器执行协程1和协程2,但他俩不会并行执行。另外在协程2执行一次后就退出整个程序,需要注意的是fmt库是会向标准输出的写入内容的,也就是说fmt.Print函数的执行是发生了系统调用的。
上面的代码无论我们执行多少次,程序都会退出。当协程1先执行后,发生系统调用,协程2执行,协程2执行完后程序退出。或者协程2先执行然后退出。但后一种情况很少见(我没有试出来过)。
如果改一下上面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"os"
"runtime"
)

func go1() {
for {
// fmt.Print(1)
}
}
func go2() {
for {
fmt.Print(2)
os.Exit(1)
}
}
func main() {
runtime.GOMAXPROCS(2) // 设置最多有两个核心cpu,也就是两个处理器
go go1() // 启动一个协程
go go2() // 启动一个协程
for{}

}
注释掉协程1中的fmt.Print方法,再运行代码,就会看到,程序什么都不会输出,但也不退出,这是因为协程1和主协程一样无限执行,一直占用着处理器,协程1中没有任何系统调用的代码,协程1一直占用着cpu,设置了最多处理器,main和协程1各占一个,而且没有发生系统调用,协程2永远得不到调度。
同理,将 runtime.GOMAXPROCS(3) 设置为3,那么执行结果肯定程序会正常退出,原因就不在解释了。
下面再改一下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"os"
"runtime"
)

func go1() {
for {
//fmt.Print(1)
}
}
func go2() {
for {
fmt.Print(2)
os.Exit(1)
}
}
func main() {
runtime.GOMAXPROCS(2) // 设置最多有两个核心cpu,也就是两个处理器
go go1() // 启动一个协程
go go2() // 启动一个协程
select{} // for 改为select

}
将主协程的for改为select,select是会阻塞的,所以主协程占用的处理器得到了释放,用来执行协程2来,所以程序也可以退出。
作者

itpika

发布于

2020-02-02 13:26:50

更新于

2021-01-18 13:58:58

许可协议

评论