> ## Content Index
> Fetch the complete content index at: https://blog.hacking.pub/llms.txt
> Use this file to discover other available public pages before exploring further.

# golang 异步抢占例子
- URL: https://blog.hacking.pub/2020/09/04/golang-yi-bu-qiang-zhan-li-zi/
- Published: 2020-09-04T11:17:39.000Z
- Updated: 2020-09-04T11:18:05.000Z
- Author: chainhelen
- Tags: golang, go语言, go源码, godbg, 异步, 非协作式抢占, 异步抢占

golang 异步抢占例子，继上篇文章《golang 非协作式抢占》添加一些手操事例

一定要go1.14以后的版本，本文版本

```
go version go1.15.1 linux/amd64

```

源码`cat main.go`

```
package main

import (
	"fmt"
	"golang.org/x/sys/unix"
	"runtime"
	"time"
)

func main() {
	runtime.GOMAXPROCS(1)
	for {
		time.Sleep(time.Second * 5)
		go func() {
			fmt.Printf("tid %d\n", unix.Gettid())
			for {
			}
		}()
	}
	time.Sleep(time.Hour)
}

```

编译

```
go build -o main main.go

```

#### 运行

两个shell终端，一个用于执行，一个查看`strace`

##### 关闭异步抢占

第一个终端执行 `GODEBUG=asyncpreemptoff=1 ./main ` 可以看到输出了一个`tid 25674`

第二个终端执行 `strace -p 25674`，可以看到显示

![1599118327532](https://blog.hacking.pub/qiniu/golang%20%E5%BC%82%E6%AD%A5%E6%8A%A2%E5%8D%A0%E4%BE%8B%E5%AD%90/1.png)

##### 开启异步抢占

第一个终端执行 `./main`，可以看到输出了一个`tid 3141`

第二终端执行`strace -p 3141` 可以看到右侧显示大量的SIGURG信号

![1599118327532](https://blog.hacking.pub/qiniu/golang%20%E5%BC%82%E6%AD%A5%E6%8A%A2%E5%8D%A0%E4%BE%8B%E5%AD%90/2.png)

注意到右侧的si\_pid 与 tid不同，也就是虽然 GMP中虽然 P被设置了1，但是 M这个时候数量是多于 P的。  
发送的信号之后主要做的是保存 ctx，接着 M与P depatch掉。（

###### GC

通常发生在饥饿态。当然如果搞复杂一点，也可以弄一个 `GC` 想要 stop the word，但是同步抢占无法 depatch其中一个P。我们知道 stop the word，所有的M需要与 P depath，然后进入`idle`列表。  
这种无法`depathc` 会拉长整个 GC时间，甚至是挂起整个进程。上篇《golang 非协作抢占》有链接可以看一下奇葩的事例。所以异步抢占，是有利于 GC的，但是整体来说，基于信号的方式，性能非常差。

##### 调试器

对于调试器来说，会拦截到的本该进程接受到的`SIGURG`，所以需要对其忽略，这个是golang异步占用的原提议提到的（看我上篇文章）

golang的dlv [https://github.com/go-delve/delve/issues/1754](https://github.com/go-delve/delve/issues/1754?ref=blog.hacking.pub) 会收到影响

比方说我写的go调试器toy，里面需要将[https://github.com/chainhelen/godbg/pull/6](https://github.com/chainhelen/godbg/pull/6?ref=blog.hacking.pub) 信号忽略

#### tight loop

另外需要注意的是可能有人说，上面这个例子`for`是个死循环，正常不会写这种代码。

但是实际生产环境，由于编译器不添加 `gcflag='-N -l'`，SSA阶段会做大量的指令和内联优化，

是有可能把函数调用优化成上述的代码，当然这是另一种go用法的失误（或者说go设计者并不希望你这么用，详细可以看上篇《golang 非协作抢占》里面有几个链接都是比较好的事例）。

#### 其他BUG

1. 正常运行的时候，` GODEBUG=asyncpreemptoff=1`开关关闭，就是说不允许异步抢占（进程自己不发送`SIGURG emit`），那么就不会发生协程异步抢占了吗？哈哈，有人就是手贱，手动发送。[https://github.com/golang/go/issues/38531](https://github.com/golang/go/issues/38531?ref=blog.hacking.pub)
2. 还有一些跑满CPU的bug，[https://github.com/golang/go/issues/37741](https://github.com/golang/go/issues/37741?ref=blog.hacking.pub)
3. 还有有人提议把runtime 自己产生的信号，过滤掉，不要发送到用户态的代码 [https://github.com/golang/go/issues/37942](https://github.com/golang/go/issues/37942?ref=blog.hacking.pub)

#### 代码位置

**下面是分析源码，不喜欢看的可以跳过。**  
先找一下发送信号的位置。先看最最基本的，某个G占用超过10ms的情况，由 monitor 协程发起的。

###### 发送方

```
// src/runtime/proc.go
func sysmon() {
	...
		// retake P's blocked in syscalls
		// and preempt long running G's
		if retake(now) != 0 {
			idle = 0
		} else {
			idle++
		}
	...
}

```

上面的这个`retake`对所有的G进行重整

```
// src/runtime/proc.go

func retake(now int64) uint32 {
	for i := 0; i < len(allp); i++ {
    ...
		if s == _Prunning || s == _Psyscall {
			// Preempt G if it's running for too long.
			t := int64(_p_.schedtick)
			
			// 这地方就是协程自己调度了，没有被抢占
			// 所以G对应的schedtick和schedwhen跟监控的不一致，需要重新更新一些monitor的数值
			
			if int64(pd.schedtick) != t {
				pd.schedtick = uint32(t)
				pd.schedwhen = now
			} else if pd.schedwhen+forcePreemptNS <= now {
			
			    // 如果超过了10ms就需要进行抢占了
			    
				preemptone(_p_)
				// In case of syscall, preemptone() doesn't
				// work, because there is no M wired to P.
				sysretake = true
			}
		}
	...
	}
}

```

上面遍历所有的P，monotor发现上次我就监控过你，这次发现你了，然后还时间间隔超了10ms，那对不起了，我要执行 `preemptone(_p_)`了。

```
func preemptone(_p_ *p) bool {
	...

    // 注意这个两个属性其实是重复的，就是说作用是一样的，都是为了标记当前是在抢占中
    // 可以看一下preempt的注解
	gp.preempt = true
	gp.stackguard0 = stackPreempt

	// Request an async preemption of this P.
	if preemptMSupported && debug.asyncpreemptoff == 0 {
		_p_.preempt = true
		preemptM(mp)
	}

	...
}

```

在之前的版本协作式抢占中，这个标记设置好了，就只能干等着，直到 morestask (函数调用)或者gopark（IO操作，例如channel之类）才行。但是这里面明显就能看到有一个 preemptM信号通知线程。

###### 接收方

```
./src/runtime/signal_unix.go

func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
	if sig == sigPreempt && debug.asyncpreemptoff == 0 {
		// Might be a preemption signal.
		doSigPreempt(gp, c)
		// Even if this was definitely a preemption signal, it
		// may have been coalesced with another signal, so we
		// still let it through to the application.
	}
}

```

上面是发送信号的地方，`sigPreempt`是一个常量，该文件能看到 `const sigPreempt = _SIGURG`

```
./src/runtime/signal_unix.go

// doSigPreempt handles a preemption signal on gp.
func doSigPreempt(gp *g, ctxt *sigctxt) {
	// Check if this G wants to be preempted and is safe to
	// preempt.
	if wantAsyncPreempt(gp) {
		if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {
			// Adjust the PC and inject a call to asyncPreempt.
			ctxt.pushCall(funcPC(asyncPreempt), newpc)
		}
	}

	// Acknowledge the preemption.
	atomic.Xadd(&gp.m.preemptGen, 1)
	atomic.Store(&gp.m.signalPending, 0)
}

```

这里面可以看到需要判断`异步安全点`的（就是上篇那个那个，自己看），然后返回一个可以植入的`pc`寄存器位置。

```
./src/runtime/preempt.go

func isAsyncSafePoint(gp *g, pc, sp, lr uintptr) (bool, uintptr) {
	....
	
	// 上面别的地方check还挺简单的，就不贴了。
	// 但是这里面我一开始没看懂第二个条件。
	// 因为我看到asyncPreemptStack的赋值 var asyncPreemptStack = ^uintptr(0)
	// 是-1的补码，就是说如果是无符号，那么就是最大的整数
	// 所以怎么可能两个数相减超过最大（二进制全1）
	// 后来发现func init()函数里面对asyncPreemptStack重新赋值了。。。。
	
	// 实际第二个条件它的含义是检查的是当前G是否还够栈插入异步抢占栈空间。
	// 而且第一个条件是不能省掉的，仅用第二个条件因为可能相减（无符号）存在溢出
	// （在不stack split的情况下）
	//  
	//          ——ho      |
	//          |         |
	// bp——---- |         |
	//   |      |         |
	//   |      |         |
	//   |      |         |
	//sp ——--   |         |
	//          |       \ | /
	//          |        \|/
	//          |      stack递减方向
	//          —— lo
	//  
	if sp < gp.stack.lo || sp-gp.stack.lo < asyncPreemptStack {
		return false, 0
	}
	
	// 底下还有一些判断，大多数跟编译器生成的信息有关，主要跟pcdata、funcdata有关
}

```

```
./src/runtime/preempt_amd64.s 

TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0

	MOVQ AX, 0(SP)
	MOVQ CX, 8(SP)
	MOVQ DX, 16(SP)
	MOVQ BX, 24(SP)
	...
	CALL ·asyncPreempt2(SB)
	...
	MOVQ 16(SP), DX
	MOVQ 8(SP), CX
	MOVQ 0(SP), AX
	

```

这部分是汇编实现的，我个人电脑是amd64/linux，所以看的是这个文件，这里面其实就是保存了所有的寄存器，然后偷偷在中间调用了asyncPreempt2（这个是golang实现的），这样让其进行了调度。再偷偷恢复了自己的寄存器。

后面代码就有点小复杂，而且并不是异步抢占的核心逻辑。看代码主要要看三个函数

```
func preemptPark(gp *g) 
func gopreempt_m(gp *g) 
func schedule()

```

补一张我画的天师神图，希望一路驱魔斩妖保平安

![](https://blog.hacking.pub/qiniu/golang%20%E5%BC%82%E6%AD%A5%E6%8A%A2%E5%8D%A0%E4%BE%8B%E5%AD%90/aysnc_preemtion.png)

#### 注意

golang的异步信号是针对（target）线程(thread)级别的，通常由`sysmon`发起抢占信号。就是说对应GMP模型中的M，而最终发生切换动作的其实是 G。

> 参考：
> 
> <[因goroutine运行时间过长而发生的抢占调度](https://cloud.tencent.com/developer/article/1450290%3E?ref=blog.hacking.pub)
> 
> [https://www.jianshu.com/p/604d277cbc6f](https://www.jianshu.com/p/604d277cbc6f?ref=blog.hacking.pub)
> 
> [https://golang.org/doc/asm](https://golang.org/doc/asm?ref=blog.hacking.pub)
> 
> [https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-06-func-again.html](https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-06-func-again.html?ref=blog.hacking.pub)
> 
> [https://medium.com/a-journey-with-go/go-asynchronous-preemption-b5194227371c](https://medium.com/a-journey-with-go/go-asynchronous-preemption-b5194227371c?ref=blog.hacking.pub)
> 
> \[[https://www.bookstack.cn/read/qcrao-Go-Questions/goroutine 调度器-sysmon 后台监控线程做了什么.md](https://www.bookstack.cn/read/qcrao-Go-Questions/goroutine%20%E8%B0%83%E5%BA%A6%E5%99%A8-sysmon%20%E5%90%8E%E5%8F%B0%E7%9B%91%E6%8E%A7%E7%BA%BF%E7%A8%8B%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88.md?ref=blog.hacking.pub)\]([https://www.bookstack.cn/read/qcrao-Go-Questions/goroutine](https://www.bookstack.cn/read/qcrao-Go-Questions/goroutine?ref=blog.hacking.pub) 调度器-sysmon 后台监控线程做了什么.md)