searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Golang深入浅出——调度器

2023-09-27 09:49:10
4
0

调度器版本演进

单线程调度器

1)该版本调度器在0.X版本实现,代码结构比较简单,是一种G-M模型。

优点:代码简单

缺点:程序只能存在一个活跃线程

2)代码实现

static void scheduler(void) {
	G* gp;
	lock(&sched);

	if(gosave(&m->sched)){
		lock(&sched);
		gp = m->curg;
		switch(gp->status){
		case Grunnable:
		case Grunning:
			gp->status = Grunnable;
			gput(gp);
			break;
		...
		}
		notewakeup(&gp->stopped);
	}

	gp = nextgandunlock();
	noteclear(&gp->stopped);
	gp->status = Grunning;
	m->curg = gp;
	g = gp;
	gogo(&gp->sched);
}

3)流程解读

3.1)获取调度器全局锁:lock(&sched);

3.2)调用runtime.gosave保存栈寄存器和程序计数器

3.3)获取需要运行的goroutine并解锁全局锁:gp = nextgandunloc()

3.4)绑定goroutine到全局线程m

3.5)执行runtime.gogo运行新的goroutine

多线程调度器

1)该调度器在golang1.0版本实现;

优点:支持多线程

缺点:a)调度器和锁是全局资源,调度状态中心化存储,锁竞争问题严重;b)每个线程都需要处理内存缓存,导致大量内存占用,并影响数据局部性,c)系统调用频繁对正在运行的线程阻塞和解除阻塞,增加额外开销。

2)代码实现

static void schedule(G *gp) {
	schedlock();
	if(gp != nil) {
		gp->m = nil;
		uint32 v = runtime·xadd(&runtime·sched.atomic, -1<<mcpuShift);
		if(atomic_mcpu(v) > maxgomaxprocs)
			runtime·throw("negative mcpu in scheduler");

		switch(gp->status){
		case Grunning:
			gp->status = Grunnable;
			gput(gp);
			break;
		case ...:
		}
	} else {
		...
	}
	gp = nextgandunlock();
	gp->status = Grunning;
	m->curg = gp;
	gp->m = m;
	runtime·gogo(&gp->sched, 0);
}

3)流程解读

执行流程与单线程调度相似

 

任务窃取调度器

1)该版本调度器在1.1版本实现,该版本在原G-M模型的基础上,引入了处理器P,并在处理器P的基础上实现了基于工作窃取的调度机制;

2)代码实现

static void schedule(void) {
    G *gp;
 top:
    if(runtime·gcwaiting) {
        gcstopm();
        goto top;
    }

    gp = runqget(m->p);
    if(gp == nil)
        gp = findrunnable();

    ...

    execute(gp);
}

3)流程解读

3.1)如果当前运行时,在等待垃圾回收,就执行runtime.gcstopm;

3.2)调用runqget从本地获取待执行的goroutine,如果获取不到,则调用runtime.findrunnable触发工作窃取,从其他处理器的队列中或从全局运行队列获取待执行的goroutine;

3.3)调用runtime.execute在当前线程M上执行goroutine

 

G-M-P模型

协程G

G代表goroutine,在golang表示一个需要执行的任务,协程通过go关键字创建,golang编译器遇到go关键字会替换成runtime.newproc函数调用。

数据结构

type g struct {
	stack       stack
	stackguard0 uintptr
	preempt       bool // 抢占信号
	preemptStop   bool // 抢占时将状态修改成 `_Gpreempted`
	preemptShrink bool // 在同步安全点收缩栈
	m              *m //当前 Goroutine 占用的线程,可能为空
	sched          gobuf// 存储 Goroutine 的调度相关的数据
	atomicstatus   uint32
	goid           int64
}

线程M

线程M是操作系统真实的物理线程,调度器做多可以创建10000个线程,最多只有GOMAXPORCS个活跃线程。

数据结构

type m struct {
	g0   *g//持有调度栈的goroutine
	curg *g//当前线程上运行的用户goroutine
	p             puintptr
	nextp         puintptr
	oldp          puintptr
}

处理器P

处理器P是线程M和协程G的中间层,它提供线程需要的上下文环境,负责调度线程上的等待队列,通过P的调度,每一个线程M可以执行多个协程G。

数据结构

type p struct {
	m           muintptr // 绑定的线程m

	runqhead uint32 // 待执行队列头
	runqtail uint32 // 待执行队列尾
	runq     [256]guintptr // 待执行的goroutine队列
	runnext guintptr // 下一个要执行的goroutine
	...
}
0条评论
0 / 1000
s****n
5文章数
0粉丝数
s****n
5 文章 | 0 粉丝
原创

Golang深入浅出——调度器

2023-09-27 09:49:10
4
0

调度器版本演进

单线程调度器

1)该版本调度器在0.X版本实现,代码结构比较简单,是一种G-M模型。

优点:代码简单

缺点:程序只能存在一个活跃线程

2)代码实现

static void scheduler(void) {
	G* gp;
	lock(&sched);

	if(gosave(&m->sched)){
		lock(&sched);
		gp = m->curg;
		switch(gp->status){
		case Grunnable:
		case Grunning:
			gp->status = Grunnable;
			gput(gp);
			break;
		...
		}
		notewakeup(&gp->stopped);
	}

	gp = nextgandunlock();
	noteclear(&gp->stopped);
	gp->status = Grunning;
	m->curg = gp;
	g = gp;
	gogo(&gp->sched);
}

3)流程解读

3.1)获取调度器全局锁:lock(&sched);

3.2)调用runtime.gosave保存栈寄存器和程序计数器

3.3)获取需要运行的goroutine并解锁全局锁:gp = nextgandunloc()

3.4)绑定goroutine到全局线程m

3.5)执行runtime.gogo运行新的goroutine

多线程调度器

1)该调度器在golang1.0版本实现;

优点:支持多线程

缺点:a)调度器和锁是全局资源,调度状态中心化存储,锁竞争问题严重;b)每个线程都需要处理内存缓存,导致大量内存占用,并影响数据局部性,c)系统调用频繁对正在运行的线程阻塞和解除阻塞,增加额外开销。

2)代码实现

static void schedule(G *gp) {
	schedlock();
	if(gp != nil) {
		gp->m = nil;
		uint32 v = runtime·xadd(&runtime·sched.atomic, -1<<mcpuShift);
		if(atomic_mcpu(v) > maxgomaxprocs)
			runtime·throw("negative mcpu in scheduler");

		switch(gp->status){
		case Grunning:
			gp->status = Grunnable;
			gput(gp);
			break;
		case ...:
		}
	} else {
		...
	}
	gp = nextgandunlock();
	gp->status = Grunning;
	m->curg = gp;
	gp->m = m;
	runtime·gogo(&gp->sched, 0);
}

3)流程解读

执行流程与单线程调度相似

 

任务窃取调度器

1)该版本调度器在1.1版本实现,该版本在原G-M模型的基础上,引入了处理器P,并在处理器P的基础上实现了基于工作窃取的调度机制;

2)代码实现

static void schedule(void) {
    G *gp;
 top:
    if(runtime·gcwaiting) {
        gcstopm();
        goto top;
    }

    gp = runqget(m->p);
    if(gp == nil)
        gp = findrunnable();

    ...

    execute(gp);
}

3)流程解读

3.1)如果当前运行时,在等待垃圾回收,就执行runtime.gcstopm;

3.2)调用runqget从本地获取待执行的goroutine,如果获取不到,则调用runtime.findrunnable触发工作窃取,从其他处理器的队列中或从全局运行队列获取待执行的goroutine;

3.3)调用runtime.execute在当前线程M上执行goroutine

 

G-M-P模型

协程G

G代表goroutine,在golang表示一个需要执行的任务,协程通过go关键字创建,golang编译器遇到go关键字会替换成runtime.newproc函数调用。

数据结构

type g struct {
	stack       stack
	stackguard0 uintptr
	preempt       bool // 抢占信号
	preemptStop   bool // 抢占时将状态修改成 `_Gpreempted`
	preemptShrink bool // 在同步安全点收缩栈
	m              *m //当前 Goroutine 占用的线程,可能为空
	sched          gobuf// 存储 Goroutine 的调度相关的数据
	atomicstatus   uint32
	goid           int64
}

线程M

线程M是操作系统真实的物理线程,调度器做多可以创建10000个线程,最多只有GOMAXPORCS个活跃线程。

数据结构

type m struct {
	g0   *g//持有调度栈的goroutine
	curg *g//当前线程上运行的用户goroutine
	p             puintptr
	nextp         puintptr
	oldp          puintptr
}

处理器P

处理器P是线程M和协程G的中间层,它提供线程需要的上下文环境,负责调度线程上的等待队列,通过P的调度,每一个线程M可以执行多个协程G。

数据结构

type p struct {
	m           muintptr // 绑定的线程m

	runqhead uint32 // 待执行队列头
	runqtail uint32 // 待执行队列尾
	runq     [256]guintptr // 待执行的goroutine队列
	runnext guintptr // 下一个要执行的goroutine
	...
}
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0