golang
最大一特色便是其原生支持goroutine
,轻量的goroutine
的实现可以使得程序轻松支持超高并发。想要深入学习golang
,少不了详细理解goroutine
原理。
goroutine
是golang
实现的用户级线程,其具有以下几个特点:
- 启动代价小
- 栈大小可以动态伸缩,初始大小2KB
- 线程在用户态调度,切换线程无需在用户态和内核态之间来回切换
总之,相比于线程,goroutine
十分轻量。然而由于是在用户态调度线程,因此goroutine
的调度也变的比较复杂,golang
中goroutine
的调度主要依赖于GMP
模型,本系列文章也主要是围绕GMP
模型的相互配合而展开。
简版GMP
为了更好的理解GMP
模型,我们先尝试使用Java
来实现一版简单的goroutine
。线程的本质,其实也就是对任务的封装。因此对于coroutine
的本质,还是一个可执行的Task
。
首先,定义goroutine
接口:
public interface Goroutine {
void run();
}
这个接口是对每个goroutine
的封装。
其次,定义Machine
对象:
public class Machine {
//全局任务队列
private LinkedBlockingQueue<Goroutine> queue;
//OS Thread id
private int id;
public Machine(LinkedBlockingQueue<Goroutine> queue, int id) {
this.queue = queue;
this.id = id;
}
public void run() {
//开启OS Thread,从队列中获取任务并执行
new Thread(() -> {
for (; ; ) {
try {
//获取任务
Goroutine goroutine = queue.take();
//执行任务
goroutine.run();
} catch (InterruptedException e) {
return;
}
}
}).start();
}
}
Machine
主要用来启动OS Thread
,然后不断地从全局队列中获取任务并执行。
最后,封装Coroutine
类,使其可以方便的对外暴漏功能:
public class Coroutine {
public final static int CPU_NUM = 4;
//初始化全局队列
private LinkedBlockingQueue<Goroutine> queue = new LinkedBlockingQueue<>();
public void start() {
//启动`CPU`数量的Machine,
for (int i = 0; i < CPU_NUM; i++) {
Machine machine = new Machine(queue,i);
machine.run();
}
}
//提交goroutine方法,对应golang的go关键字
public boolean run(Goroutine goroutine) {
try {
queue.put(goroutine);
} catch (InterruptedException e) {
return false;
}
return true;
}
}
Coroutine
启动了CPU
数量的Machine
,同时初始化全局队列,并对外暴露run
方法用来提交任务。
到这里,简单的goroutine
便成功实现,我们可以尝试简单的使用:
public static void main(String[] args) {
//初始化Coroutine
Coroutine coroutine = new Coroutine();
coroutine.start();
//启动10个gorouine,每个goroutine循环打印1-10
for (int i = 0; i < 10; i++) {
coroutine.run(() -> {
for (int j = 0; j < 10; j++) {
System.out.println(j);
}
});
}
}
熟悉Java
的同学可能一眼看出来,这不就是一个线程池么?
GMP
的本质便是一个全局的线程池,在golang
的GMP
模型中,M
便是负责真正执行任务的OS Thread
,G
则是对任务的封装。P
则是本地任务队列。
这里先讲一讲P
的由来,从上面的简版的Goroutine
实现来看,所有的Machine
对象共享一个全局任务队列,这会使得每次添加任务和获取任务都需要加锁,这使得调度的开销非常大,借助局部性原理,我们可以给Machine
添加一个本地队列,这个队列会和Machine
进行绑定,当Machine
执行的代码会创建的新的goroutine
的时候,会优先新创建的goroutine
保存在本地,当本地容量不足时再存放在全局队列中。这里的本地队列便是P
。
可以看到一个P
在同一时间只属于一个M
,因此不存在加解锁的开销。
进一步,根据上面的简版的Goroutine
的实现,我们提出当前实现版本会遇到的问题:
- 当存在
goroutine
执行系统调用导致线程阻塞时(例如IO
),这会影响整个系统的调度 - 当存在某个
goroutine
需要执行的任务过多,导致占用OS Thread
时间过长,这会影响到队列中其他goroutine
的执行 - 当存在某个
goroutine
需要暂时挂起,例如sleep
或等待条件唤醒时,如何将当前gouroutine
暂时踢出队列。 - 加入本地队列
P
的实现之后,可能存在某个P
存储的任务非常多,而某个P
一个任务没有,这种情况应该如何解决? - …
我们接下来通过分析golang
GMP
相关源码来看golang
是如何处理以上问题。