目录

▍简单概念

▍调度器的三个抽象概念:G、M、P

▍调度的大致轮廓

下面从程序启动、调度循环、G的来源三个角度分析调度的实现。

▍进程启动时都做了什么?

▍runtime.osinit(SB)方法针对系统环境的初始化

▍runtime.schedinit(SB)调度相关的一些初始化

▍runtime·mainPC(SB)启动监控任务

▍最后 runtime·mstart(SB)启动调度循环

▍调度循环都做了什么

▍调度器如何开启调度循环

▍调度器如何进行调度循环

▍多个线程下如何调度

▍调度循环中如何让出CPU

▍正常完成让出CPU

▍主动让出CPU

▍抢占让出CPU

▍系统调用让出 CPU

▍待执行G的来源

▍gofunc 创建G

▍epoll 来源

▍看几个主动让出 CPU 的场景

▍time.Sleep

▍Mutex

▍channel

▍END


▍简单概念

调度器的三个抽象概念:G、M、P

  • G:代表一个 goroutine,每个 goroutine 都有自己独立的栈存放当前的运行内存及状态。可以把一个G当做一个任务。

  • M: 代表内核线程(Pthread),它本身就与一个内核线程进行绑定,goroutine 运行在M上。

  • P:代表一个处理器,可以认为一个“有运行任务”的P占了一个CPU线程的资源,且只要处于调度的时候就有P。

注:内核线程和 CPU 线程的区别,在系统里可以有上万个内核线程,但 CPU 线程并没有那么多,CPU 线程也就是 Top 命令里看到的 CPU0、CPU1、CPU2......的数量。

三者关系大致如下图:

图1、图2代表2个有运行任务时的状态。M 与一个内核线程绑定,可运行的 goroutine 列表存放到P里面,然后占用了一个CPU线程来运行。

图3代表没有运行任务时的状态,M 依然与一个内核线程绑定,由于没有运行任务因此不占用 CPU 线程,同时也不占用P。

调度的大致轮廓

图中表述了由 go func 触发的调度。先创建M通过M启动调度循环,然后调度循环过程中获取G来执行,执行过程中遇到图中 running G 后面几个 case 再次进入下一循环。

下面从程序启动、调度循环、G的来源三个角度分析调度的实现。

进程启动时都做了什么?

下面先看一段程序启动的代码

  1. // runtime/asm_amd64.s

  2. TEXT runtime·rt0_go(SB),NOSPLIT,$0

  3. ......此处省略N多代码......

  4. ok:

  5. // set the per-goroutine and per-mach "registers"

  6. get_tls(BX) // 将 g0 放到 tls(thread local storage)里

  7. LEAQ runtime·g0(SB), CX

  8. MOVQ CX, g(BX)

  9. LEAQ runtime·m0(SB), AX

  10. // save m->g0 = g0 // 将全局M0与全局G0绑定

  11. MOVQ CX, m_g0(AX)

  12. // save m0 to g0->m

  13. MOVQ AX, g_m(CX)

  14. CLD // convention is D is always left cleared

  15. CALL runtime·check(SB)

  16. MOVL 16(SP), AX // copy argc

  17. MOVL AX, 0(SP)

  18. MOVQ 24(SP), AX // copy argv

  19. MOVQ AX, 8(SP)

  20. CALL runtime·args(SB) // 解析命令行参数

  21. CALL runtime·osinit(SB) // 只初始化了CPU核数

  22. CALL runtime·schedinit(SB) // 内存分配器、栈、P、GC回收器等初始化

  23. // create a new goroutine to start program

  24. MOVQ $runtime·mainPC(SB), AX //

  25. PUSHQ AX

  26. PUSHQ $0 // arg size

  27. CALL runtime·newproc(SB) // 创建一个新的G来启动runtime.main

  28. POPQ AX

  29. POPQ AX

  30. // start this M

  31. CALL runtime·mstart(SB) // 启动M0,开始等待空闲G,正式进入调度循环

  32. MOVL $0xf1, 0xf1 // crash

  33. RET

在启动过程里主要做了这三个事情(这里只跟调度相关的):

  • 初始化固定数量的P

  • 创建一个新的G来启动 runtime.main, 也就是 runtime 下的 main 方法

  • 创建全局 M0、全局 G0,启动 M0 进入第一个调度循环

M0 是什么?程序里会启动多个 M,第一个启动的叫 M0。

G0 是什么?G 分三种,第一种是执行用户任务的叫做 G,第二种执行 runtime 下调度工作的叫G0,每个M都绑定一个G0。第三种则是启动 runtime.main 用到的G。写程序接触到的基本都是第一种

我们按照顺序看是怎么完成上面三个事情的。

▍runtime.osinit(SB)方法针对系统环境的初始化

这里实质只做了一件事情,就是获取 CPU 的线程数,也就是 Top 命令里看到的 CPU0、CPU1、CPU2......的数量。

  1. // runtime/os_linux.go

  2. func osinit() {

  3. ncpu = getproccount()

  4. }

▍runtime.schedinit(SB)调度相关的一些初始化

  1. // runtime/proc.go

  2. // 设置最大M数量

  3. sched.maxmcount = 10000

  4. // 初始化当前M,即全局M0

  5. mcommoninit(_g_.m)

  6. // 查看应该启动的P数量,默认为cpu core数.

  7. // 如果设置了环境变量GOMAXPROCS则以环境变量为准,最大不得超过_MaxGomaxprocs(1024)个

  8. procs := ncpu

  9. if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {

  10. procs = n

  11. }

  12. if procs > _MaxGomaxprocs {

  13. procs = _MaxGomaxprocs

  14. }

  15. // 调整P数量,此时由于是初始化阶段,所以P都是新建的

  16. if procresize(procs) != nil {

  17. throw("unknown runnable goroutine during bootstrap")

  18. }

这里 sched.maxmcount 设置了M最大的数量,而M代表的是系统内核线程,因此可以认为一个进程最大只能启动10000个系统线程。

procresize 初始化P的数量,procs 参数为初始化的数量,而在初始化之前先做数量的判断,默认是 ncpu(与CPU核数相等)。也可以通过环境变量 GOMAXPROCS 来控制P的数量。_MaxGomaxprocs 控制了最大的P数量只能是1024。

有些人在进程初始化的时候经常用到 runtime.GOMAXPROCS() 方法,其实也是调用的 procresize 方法重新设置了最大 CPU 使用数量。

▍runtime·mainPC(SB)启动监控任务

  1. // runtime/proc.go

  2. // The main goroutine.

  3. func main() {

  4. ......

  5. // 启动后台监控

  6. systemstack(func() {

  7. newm(sysmon, nil)

  8. })

  9. ......

  10. }

在 runtime 下会启动一个全程运行的监控任务,该任务用于标记抢占执行过长时间的G,以及检测 epoll 里面是否有可执行的G。下面会详细说到。

最后 runtime·mstart(SB)启动调度循环

前面都是各种初始化操作,在这里开启了调度器的第一个调度循环。(这里启动的M就是M0)

下面来围绕G、M、P三个概念介绍 Goroutine 调度循环的运作流程。

调度循环都做了什么

图1代表M启动的过程,把M跟一个P绑定再一起。在程序初始化的过程中说到在进程启动的最后一步启动了第一个M(即M0),这个M从全局的空闲P列表里拿到一个P,然后与其绑定。而P里面有2个管理G的链表(runq 存储等待运行的G列表,gfree 存储空闲的G列表),M启动后等待可执行的G。

图2代表创建G的过程。创建完一个G先扔到当前P的 runq 待运行队列里。在图3的执行过程里,M从绑定的P的 runq 列表里获取一个G来执行。当执行完成后,图4的流程里把G仍到 gfree 队列里。注意此时G并没有销毁(只重置了G的栈以及状态),当再次创建G的时候优先从 gfree 列表里获取,这样就起到了复用G的作用,避免反复与系统交互创建内存。

M即启动后处于一个自循环状态,执行完一个G之后继续执行下一个G,反复上面的图2~图4过程。当第一个M正在繁忙而又有新的G需要执行时,会再开启一个M来执行。

下面详细看下调度循环的实现。

调度器如何开启调度循环

先看一下M的启动过程(M0启动是个特殊的启动过程,也是第一个启动的M,由汇编实现的初始化后启动,而后续的M创建以及启动则是Go代码实现)。

  1. // runtime/proc.go

  2. func startm(_p_ *p, spinning bool) {

  3. lock(&sched.lock)

  4. if _p_ == nil {

  5. // 从空闲P里获取一个

  6. _p_ = pidleget()

  7. ......

  8. }

  9. // 获取一个空闲的m

  10. mp := mget()

  11. unlock(&sched.lock)

  12. // 如果没有空闲M,则new一个

  13. if mp == nil {

  14. var fn func()

  15. if spinning {

  16. // The caller incremented nmspinning, so set m.spinning in the new M.

  17. fn = mspinning

  18. }

  19. newm(fn, _p_)

  20. return

  21. }

  22. ......

  23. // 唤醒M

  24. notewakeup(&mp.park)

  25. }

  26. func newm(fn func(), _p_ *p) {

  27. // 创建一个M对象,且与P关联

  28. mp := allocm(_p_, fn)

  29. // 暂存P

  30. mp.nextp.set(_p_)

  31. mp.sigmask = initSigmask

  32. ......

  33. execLock.rlock() // Prevent process clone.

  34. // 创建系统内核线程

  35. newosproc(mp, unsafe.Pointer(mp.g0.stack.hi))

  36. execLock.runlock()

  37. }

  38. // runtime/os_linux.go

  39. func newosproc(mp *m, stk unsafe.Pointer) {

  40. // Disable signals during clone, so that the new thread starts

  41. // with signals disabled. It will enable them in minit.

  42. var oset sigset

  43. sigprocmask(_SIG_SETMASK, &sigset_all, &oset)

  44. ret := clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(funcPC(mstart)))

  45. sigprocmask(_SIG_SETMASK, &oset, nil)

  46. }

  47. func allocm(_p_ *p, fn func()) *m {

  48. ......

  49. mp := new(m)

  50. mp.mstartfn = fn // 设置启动函数

  51. mcommoninit(mp) // 初始化m

  52. // 创建g0

  53. // In case of cgo or Solaris, pthread_create will make us a stack.

  54. // Windows and Plan 9 will layout sched stack on OS stack.

  55. if iscgo || GOOS == "solaris" || GOOS == "windows" || GOOS == "plan9" {

  56. mp.g0 = malg(-1)

  57. } else {

  58. mp.g0 = malg(8192 * sys.StackGuardMultiplier)

  59. }

  60. // 把新创建的g0与M做关联

  61. mp.g0.m = mp

  62. ......

  63. return mp

  64. }

  65. func mstart() {

  66. ......

  67. mstart1()

  68. }

  69. func mstart1() {

  70. ......

  71. // 进入调度循环(阻塞不返回)

  72. schedule()

  73. }

非M0的启动首先从 startm 方法开始启动,要进行调度工作必须有调度处理器P,因此先从空闲的P链表里获取一个P,在 newm 方法创建一个M与P绑定。

newm 方法中通过 newosproc 新建一个内核线程,并把内核线程与M以及 mstart 方法进行关联,这样内核线程执行时就可以找到M并且找到启动调度循环的方法。最后 schedule 启动调度循环

allocm 方法中创建M的同时创建了一个G与自己关联,这个G就是我们在上面说到的g0。为什么M要关联一个g0?因为 runtime 下执行一个G也需要用到栈空间来完成调度工作,而拥有执行栈的地方只有G,因此需要为每个执行线程里配置一个g0。

▍调度器如何进行调度循环

调用 schedule 进入调度器的调度循环后,在这个方法里永远不再返回。下面看下实现。

  1. // runtime/proc.go

  2. func schedule() {

  3. _g_ := getg()

  4. // 进入gc MarkWorker 工作模式

  5. if gp == nil && gcBlackenEnabled != 0 {

  6. gp = gcController.findRunnableGCWorker(_g_.m.p.ptr())

  7. }

  8. if gp == nil {

  9. // Check the global runnable queue once in a while to ensure fairness.

  10. // Otherwise two goroutines can completely occupy the local runqueue

  11. // by constantly respawning each other.

  12. // 每处理n个任务就去全局队列获取G任务,确保公平

  13. if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {

  14. lock(&sched.lock)

  15. gp = globrunqget(_g_.m.p.ptr(), 1)

  16. unlock(&sched.lock)

  17. }

  18. }

  19. // 从P本地获取

  20. if gp == nil {

  21. gp, inheritTime = runqget(_g_.m.p.ptr())

  22. if gp != nil && _g_.m.spinning {

  23. throw("schedule: spinning with local work")

  24. }

  25. }

  26. // 从其它地方获取G,如果获取不到则沉睡M,并且阻塞在这里,直到M被再次使用

  27. if gp == nil {

  28. gp, inheritTime = findrunnable() // blocks until work is available

  29. }

  30. ......

  31. // 执行找到的G

  32. execute(gp, inheritTime)

  33. }

  34. // 从P本地获取一个可运行的G

  35. func runqget(_p_ *p) (gp *g, inheritTime bool) {

  36. // If there's a runnext, it's the next G to run.

  37. // 优先从runnext里获取一个G,如果没有则从runq里获取

  38. for {

  39. next := _p_.runnext

  40. if next == 0 {

  41. break

  42. }

  43. if _p_.runnext.cas(next, 0) {

  44. return next.ptr(), true

  45. }

  46. }

  47. // 从队头获取

  48. for {

  49. h := atomic.Load(&_p_.runqhead) // load-acquire, synchronize with other consumers

  50. t := _p_.runqtail

  51. if t == h {

  52. return nil, false

  53. }

  54. gp := _p_.runq[h%uint32(len(_p_.runq))].ptr()

  55. if atomic.Cas(&_p_.runqhead, h, h+1) { // cas-release, commits consume

  56. return gp, false

  57. }

  58. }

  59. }

  60. // 从其它地方获取G

  61. func findrunnable() (gp *g, inheritTime bool) {

  62. ......

  63. // 从本地队列获取

  64. if gp, inheritTime := runqget(_p_); gp != nil {

  65. return gp, inheritTime

  66. }

  67. // 全局队列获取

  68. if sched.runqsize != 0 {

  69. lock(&sched.lock)

  70. gp := globrunqget(_p_, 0)

  71. unlock(&sched.lock)

  72. if gp != nil {

  73. return gp, false

  74. }

  75. }

  76. // 从epoll里取

  77. if netpollinited() && sched.lastpoll != 0 {

  78. if gp := netpoll(false); gp != nil { // non-blocking

  79. ......

  80. return gp, false

  81. }

  82. }

  83. ......

  84. // 尝试4次从别的P偷

  85. for i := 0; i < 4; i++ {

  86. for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {

  87. if sched.gcwaiting != 0 {

  88. goto top

  89. }

  90. stealRunNextG := i > 2 // first look for ready queues with more than 1 g

  91. // 在这里开始针对P进行偷取操作

  92. if gp := runqsteal(_p_, allp[enum.position()], stealRunNextG); gp != nil {

  93. return gp, false

  94. }

  95. }

  96. }

  97. }

  98. // 尝试从全局runq中获取G

  99. // 在"sched.runqsize/gomaxprocs + 1"、"max"、"len(_p_.runq))/2"三个数字中取最小的数字作为获取的G数量

  100. func globrunqget(_p_ *p, max int32) *g {

  101. if sched.runqsize == 0 {

  102. return nil

  103. }

  104. n := sched.runqsize/gomaxprocs + 1

  105. if n > sched.runqsize {

  106. n = sched.runqsize

  107. }

  108. if max > 0 && n > max {

  109. n = max

  110. }

  111. if n > int32(len(_p_.runq))/2 {

  112. n = int32(len(_p_.runq)) / 2

  113. }

  114. sched.runqsize -= n

  115. if sched.runqsize == 0 {

  116. sched.runqtail = 0

  117. }

  118. gp := sched.runqhead.ptr()

  119. sched.runqhead = gp.schedlink

  120. n--

  121. for ; n > 0; n-- {

  122. gp1 := sched.runqhead.ptr()

  123. sched.runqhead = gp1.schedlink

  124. runqput(_p_, gp1, false) // 放到本地P里

  125. }

  126. return gp

  127. }

schedule 中首先尝试从P本地队列中获取(runqget)一个可执行的G,如果没有则从其它地方获取(findrunnable),最终通过 execute 方法执行G。

runqget 先通过 runnext 拿到待运行G,没有的话,再从 runq 里面取。

findrunnable 从全局队列、epoll、别的P里获取。(后面会扩展分析实现)

在调度的开头出还做了一个小优化:每处理一些任务之后,就优先从全局队列里获取任务,以保障公平性,防止由于每个P里的G过多,而全局队列里的任务一直得不到执行机会。

这里用到了一个关键方法getg(),runtime 的代码里大量使用该方法,它由汇编实现,该方法就是获取当前运行的G,具体实现不再这里阐述。

多个线程下如何调度

抛出一个问题:每个P里面的G执行时间是不可控的,如果多个P同时在执行,会不会出现有的P里面的G执行不完,有的P里面几乎没有G可执行呢?

这就要从M的自循环过程中如何获取G、归还G的行为说起了,先看图:

图中可以看出有两种途径:1.借助全局队列 sched.runq 作为中介,本地P里的G太多的话就放全局里,G太少的话就从全局取。2.全局列表里没有的话直接从P1里偷取(steal)。(更多M在执行的话,同样的原理,这里就只拿2个来举例)

第1种途径实现如下:

  1. // runtime/proc.go

  2. func runqput(_p_ *p, gp *g, next bool) {

  3. if randomizeScheduler && next && fastrand()%2 == 0 {

  4. next = false

  5. }

  6. // 尝试把G添加到P的runnext节点,这里确保runnext只有一个G,如果之前已经有一个G则踢出来放到runq里

  7. if next {

  8. retryNext:

  9. oldnext := _p_.runnext

  10. if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {

  11. goto retryNext

  12. }

  13. if oldnext == 0 {

  14. return

  15. }

  16. // 把老的g踢出来,在下面放到runq里

  17. gp = oldnext.ptr()

  18. }

  19. retry:

  20. // 如果_p_.runq队列不满,则放到队尾就结束了。

  21. // 试想如果不放到队尾而放到队头里会怎样?如果频繁的创建G则可能后面的G总是不被执行,对后面的G不公平

  22. h := atomic.Load(&_p_.runqhead) // load-acquire, synchronize with consumers

  23. t := _p_.runqtail

  24. if t-h < uint32(len(_p_.runq)) {

  25. _p_.runq[t%uint32(len(_p_.runq))].set(gp)

  26. atomic.Store(&_p_.runqtail, t+1) // store-release, makes the item available for consumption

  27. return

  28. }

  29. //如果队列满了,尝试把G和当前P里的一部分runq放到全局队列

  30. //因为操作全局需要加锁,所以名字里带个slow

  31. if runqputslow(_p_, gp, h, t) {

  32. return

  33. }

  34. // the queue is not full, now the put above must succeed

  35. goto retry

  36. }

  37. func runqputslow(_p_ *p, gp *g, h, t uint32) bool {

  38. var batch [len(_p_.runq)/2 + 1]*g

  39. // First, grab a batch from local queue.

  40. n := t - h

  41. n = n / 2

  42. if n != uint32(len(_p_.runq)/2) {

  43. throw("runqputslow: queue is not full")

  44. }

  45. // 从runq头部开始取出一半的runq放到临时变量batch里

  46. for i := uint32(0); i < n; i++ {

  47. batch[i] = _p_.runq[(h+i)%uint32(len(_p_.runq))].ptr()

  48. }

  49. if !atomic.Cas(&_p_.runqhead, h, h+n) { // cas-release, commits consume

  50. return false

  51. }

  52. // 把要put的g也放进batch去

  53. batch[n] = gp

  54. if randomizeScheduler {

  55. for i := uint32(1); i <= n; i++ {

  56. j := fastrandn(i + 1)

  57. batch[i], batch[j] = batch[j], batch[i]

  58. }

  59. }

  60. // 把取出来的一半runq组成链表

  61. for i := uint32(0); i < n; i++ {

  62. batch[i].schedlink.set(batch[i+1])

  63. }

  64. // 将一半的runq放到global队列里,一次多转移一些省得转移频繁

  65. lock(&sched.lock)

  66. globrunqputbatch(batch[0], batch[n], int32(n+1))

  67. unlock(&sched.lock)

  68. return true

  69. }

  70. func globrunqputbatch(ghead *g, gtail *g, n int32) {

  71. gtail.schedlink = 0

  72. if sched.runqtail != 0 {

  73. sched.runqtail.ptr().schedlink.set(ghead)

  74. } else {

  75. sched.runqhead.set(ghead)

  76. }

  77. sched.runqtail.set(gtail)

  78. sched.runqsize += n

  79. }

runqput 方法归还执行完的G,runq 定义是 runq [256]guintptr,有固定的长度,因此当前P里的待运行G超过256的时候说明过多了,则执行 runqputslow 方法把一半G扔给全局G链表,globrunqputbatch 连接全局链表的头尾指针。

但可能别的P里面并没有超过256,就不会放到全局G链表里,甚至可能一直维持在不到256个。这就借助第2个途径了:

第2种途径实现如下:

  1. // runtime/proc.go

  2. // 从其它地方获取G

  3. func findrunnable() (gp *g, inheritTime bool) {

  4. ......

  5. // 尝试4次从别的P偷

  6. for i := 0; i < 4; i++ {

  7. for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {

  8. if sched.gcwaiting != 0 {

  9. goto top

  10. }

  11. stealRunNextG := i > 2 // first look for ready queues with more than 1 g

  12. // 在这里开始针对P进行偷取操作

  13. if gp := runqsteal(_p_, allp[enum.position()], stealRunNextG); gp != nil {

  14. return gp, false

  15. }

  16. }

  17. }

  18. }

从别的P里面"偷取"一些G过来执行了。runqsteal 方法实现了"偷取"操作。

  1. // runtime/proc.go

  2. // 偷取P2一半到本地运行队列,失败则返回nil

  3. func runqsteal(_p_, p2 *p, stealRunNextG bool) *g {

  4. t := _p_.runqtail

  5. n := runqgrab(p2, &_p_.runq, t, stealRunNextG)

  6. if n == 0 {

  7. return nil

  8. }

  9. n--

  10. // 返回尾部的一个G

  11. gp := _p_.runq[(t+n)%uint32(len(_p_.runq))].ptr()

  12. if n == 0 {

  13. return gp

  14. }

  15. h := atomic.Load(&_p_.runqhead) // load-acquire, synchronize with consumers

  16. if t-h+n >= uint32(len(_p_.runq)) {

  17. throw("runqsteal: runq overflow")

  18. }

  19. atomic.Store(&_p_.runqtail, t+n) // store-release, makes the item available for consumption

  20. return gp

  21. }

  22. // 从P里获取一半的G,放到batch里

  23. func runqgrab(_p_ *p, batch *[256]guintptr, batchHead uint32, stealRunNextG bool) uint32 {

  24. for {

  25. // 计算一半的数量

  26. h := atomic.Load(&_p_.runqhead) // load-acquire, synchronize with other consumers

  27. t := atomic.Load(&_p_.runqtail) // load-acquire, synchronize with the producer

  28. n := t - h

  29. n = n - n/2

  30. ......

  31. // 将偷到的任务转移到本地P队列里

  32. for i := uint32(0); i < n; i++ {

  33. g := _p_.runq[(h+i)%uint32(len(_p_.runq))]

  34. batch[(batchHead+i)%uint32(len(batch))] = g

  35. }

  36. if atomic.Cas(&_p_.runqhead, h, h+n) { // cas-release, commits consume

  37. return n

  38. }

  39. }

  40. }

上面可以看出从别的P里面偷(steal)了一半,这样就足够运行了。有了“偷取”操作也就充分利用了多线程的资源。

调度循环中如何让出CPU

▍正常完成让出CPU

绝大多数场景下我们程序都是执行完一个G,再执行另一个G,那我们就看下G是如何被执行以及执行完如何退出的。

先看G如何被执行:

  1. // runtime/proc.go

  2. func execute(gp *g, inheritTime bool) {

  3. _g_ := getg()

  4. casgstatus(gp, _Grunnable, _Grunning)

  5. ......

  6. // 真正的执行G,切换到该G的栈帧上执行(汇编实现)

  7. gogo(&gp.sched)

  8. }

execute 方法先更改G的状态为_Grunning 表示运行中,最终给 gogo 方法做实际的执行操作。而 gogo 方法则是汇编实现。再来看下 gogo 方法的实现:

  1. // runtime.asm_amd64.s

  2. TEXT runtime·gogo(SB), NOSPLIT, $16-8

  3. MOVQ buf+0(FP), BX // gobuf 把0偏移的8个字节给BX寄存器, gobuf结构的前8个字节就是SP指针

  4. // If ctxt is not nil, invoke deletion barrier before overwriting.

  5. MOVQ gobuf_ctxt(BX), AX // 在把gobuf的ctxt变量给AX寄存器

  6. TESTQ AX, AX // 判断AX寄存器是否为空,传进来gp.sched的话肯定不为空了,因此JZ nilctxt不跳转

  7. JZ nilctxt

  8. LEAQ gobuf_ctxt(BX), AX

  9. MOVQ AX, 0(SP)

  10. MOVQ $0, 8(SP)

  11. CALL runtime·writebarrierptr_prewrite(SB)

  12. MOVQ buf+0(FP), BX

  13. nilctxt: // 下面则是函数栈的BP SP指针移动,最后进入到指定的代码区域

  14. MOVQ gobuf_g(BX), DX

  15. MOVQ 0(DX), CX // make sure g != nil

  16. get_tls(CX)

  17. MOVQ DX, g(CX)

  18. MOVQ gobuf_sp(BX), SP // restore SP

  19. MOVQ gobuf_ret(BX), AX

  20. MOVQ gobuf_ctxt(BX), DX

  21. MOVQ gobuf_bp(BX), BP

  22. MOVQ $0, gobuf_sp(BX) // clear to help garbage collector

  23. MOVQ $0, gobuf_ret(BX)

  24. MOVQ $0, gobuf_ctxt(BX)

  25. MOVQ $0, gobuf_bp(BX)

  26. MOVQ gobuf_pc(BX), BX // PC指针指向退出时要执行的函数地址

  27. JMP BX // 跳转到执行代码处

 
  1. // runtime/runtime2.go

  2. type gobuf struct {

  3. // The offsets of sp, pc, and g are known to (hard-coded in) libmach.

  4. //

  5. // ctxt is unusual with respect to GC: it may be a

  6. // heap-allocated funcval so write require a write barrier,

  7. // but gobuf needs to be cleared from assembly. We take

  8. // advantage of the fact that the only path that uses a

  9. // non-nil ctxt is morestack. As a result, gogo is the only

  10. // place where it may not already be nil, so gogo uses an

  11. // explicit write barrier. Everywhere else that resets the

  12. // gobuf asserts that ctxt is already nil.

  13. sp uintptr

  14. pc uintptr

  15. g guintptr

  16. ctxt unsafe.Pointer // this has to be a pointer so that gc scans it

  17. ret sys.Uintreg

  18. lr uintptr

  19. bp uintptr // for GOEXPERIMENT=framepointer

  20. }

gogo 方法传的参数注意是 gp.sched,而这个结构体里可以看到保存了熟悉的函数栈寄存器 SP/PC/BP,能想到是把执行栈传了进去(既然是执行一个G,当然要把执行栈传进去了)。可以看到在 gogo 函数中实质就只是做了函数栈指针的移动。

这个执行G的操作,熟悉函数调用的函数栈的基本原理的人想必有些印象(如果不熟悉请自行搜索),执行一个G其实就是执行函数一样切换到对应的函数栈帧上。

C语言里栈帧创建的时候有个IP寄存器指向"return address",即主调函数的一条指令的地址, 被调函数退出的时候通过该指针回到调用函数里。在Go语言里有个PC寄存器指向退出函数。那么下PC寄存器指向的是哪里?我们回到创建G的地方看下代码:

  1. // runtime/proc.go

  2. func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {

  3. ......

  4. // 从当前P里面复用一个空闲G

  5. newg := gfget(_p_)

  6. // 如果没有空闲G则新建一个,默认堆大小为_StackMin=2048 bytes

  7. if newg == nil {

  8. newg = malg(_StackMin)

  9. casgstatus(newg, _Gidle, _Gdead)

  10. // 把新创建的G添加到全局allg里

  11. allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.

  12. }

  13. ......

  14. newg.sched.sp = sp

  15. newg.stktopsp = sp

  16. newg.sched.pc = funcPC(goexit) + sys.PCQuantum // 记录当前任务的pc寄存器为goexit方法,用于当执行G结束后找到退出方法,从而再次进入调度循环 // +PCQuantum so that previous instruction is in same function

  17. newg.sched.g = guintptr(unsafe.Pointer(newg))

  18. gostartcallfn(&newg.sched, fn)

  19. newg.gopc = callerpc

  20. newg.startpc = fn.fn

  21. .......

  22. return newg

  23. }

代码中可以看到,给G的执行环境里的 pc 变量赋值了一个 goexit 的函数地址,也就是说G正常执行完退出时执行的是 goexit 函数。再看下该函数的实现:

  1. // runtime/asm_amd64.s

  2. // The top-most function running on a goroutine

  3. // returns to goexit+PCQuantum.

  4. TEXT runtime·goexit(SB),NOSPLIT,$0-0

  5. BYTE $0x90 // NOP

  6. CALL runtime·goexit1(SB) // does not return

  7. // traceback from goexit1 must hit code range of goexit

  8.   BYTE  $0x90  // NOP

 
  1. // runtime/proc.go

  2. // G执行结束后回到这里放到P的本地队列里

  3. func goexit1() {

  4. if raceenabled {

  5. racegoend()

  6. }

  7. if trace.enabled {

  8. traceGoEnd()

  9. }

  10. // 切换到g0来释放G

  11. mcall(goexit0)

  12. }

  13. // g0下当G执行结束后回到这里放到P的本地队列里

  14. func goexit0(gp *g) {

  15. ......

  16. gfput(_g_.m.p.ptr(), gp)

  17. schedule()

  18. }

代码中切换到了G0下执行了 schedule 方法,再次进度了下一轮调度循环。

以上就是正常执行一个G并正常退出的实现。

主动让出CPU

在实际场景中还有一些没有执行完成的G,而又需要临时停止执行,比如 time.Sleep、IO阻塞等等,就需要挂起该G,把CPU让出给别人使用。在 runtime 下面有个 gopark 方法,看下实现:// runtime/proc.go

  1. func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason string, traceEv byte, traceskip int) {

  2. mp := acquirem()

  3. gp := mp.curg

  4. status := readgstatus(gp)

  5. if status != _Grunning && status != _Gscanrunning {

  6. throw("gopark: bad g status")

  7. }

  8. mp.waitlock = lock

  9. mp.waitunlockf = *(*unsafe.Pointer)(unsafe.Pointer(&unlockf))

  10. gp.waitreason = reason

  11. mp.waittraceev = traceEv

  12. mp.waittraceskip = traceskip

  13. releasem(mp)

  14. // can't do anything that might move the G between Ms here.

  15. // mcall 在M里从当前正在运行的G切换到g0

  16. // park_m 在切换到的g0下先把传过来的G切换为_Gwaiting状态挂起该G

  17. // 调用回调函数waitunlockf()由外层决定是否等待解锁,返回true则等待解锁不在执行G,返回false则不等待解锁继续执行

  18. mcall(park_m)

  19. }

  1. // runtime/stubs.go

  2. // mcall switches from the g to the g0 stack and invokes fn(g),

  3. // where g is the goroutine that made the call.

  4. // mcall saves g's current PC/SP in g->sched so that it can be restored later.

  5. ......

  6. func mcall(fn func(*g))

 
  1. // runtime/proc.go

  2. func park_m(gp *g) {

  3. _g_ := getg() // 此处获得的是g0,而不是gp

  4. if trace.enabled {

  5. traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)

  6. }

  7. casgstatus(gp, _Grunning, _Gwaiting)

  8. dropg() // 把g0从M的"当前运行"里剥离出来

  9. if _g_.m.waitunlockf != nil {

  10. fn := *(*func(*g, unsafe.Pointer) bool)(unsafe.Pointer(&_g_.m.waitunlockf))

  11. ok := fn(gp, _g_.m.waitlock)

  12. _g_.m.waitunlockf = nil

  13. _g_.m.waitlock = nil

  14. if !ok { // 如果不需要等待解锁,则切换到_Grunnable状态并直接执行G

  15. if trace.enabled {

  16. traceGoUnpark(gp, 2)

  17. }

  18. casgstatus(gp, _Gwaiting, _Grunnable)

  19. execute(gp, true) // Schedule it back, never returns.

  20. }

  21. }

  22. schedule()

  23. }

gopark 是进行调度出让CPU资源的方法,里面有个方法 mcall(),注释里这样描述:

从当前运行的G切换到g0的运行栈上,然后调用fn(g),这里被调用的G是调用mcall方法时的G。mcall方法保存当前运行的G的 PC/SP 到 g->sched 里,因此该G可以在以后被重新恢复执行.

在本章开始介绍初始化过程中有提到M创建的时候绑定了一个 g0,调度工作是运行在 g0 的栈上的。mcall 方法通过 g0 先把当前调用的G的执行栈暂存到 g->sched 变量里,然后切换到 g0 的执行栈上执行 park_m。park_m 方法里把 gp 的状态从 _Grunning 切换到 _Gwaiting 表明进入到等待唤醒状态,此时休眠G的操作就完成了。接下来既然G休眠了,CPU 线程总不能闲下来,在 park_m 方法里又可以看到 schedule 方法,开始进入到到一轮调度循环了。

park_m 方法里还有段小插曲,进入调度循环之前还有个对 waitunlockf 方法的判断,该方法意思是如果解锁不成功则调用 execute 方法继续执行之前的 G,而该方法永远不会 return,也就不会再次进入下一次调度。也就是说给外部一个控制是否要进行下一个调度的选择。

抢占让出CPU

回想在 runtime.main()里面有单独启动了一个监控任务,方法是 sysmon。看下该方法:

  1. // runtime/proc.go

  2. func sysmon() {

  3. ......

  4. for {

  5. // delay参数用于控制for循环的间隔,不至于无限死循环。

  6. // 控制逻辑是前50次每次sleep 20微秒,超过50次则每次翻2倍,直到最大10毫秒

  7. if idle == 0 { // start with 20us sleep...

  8. delay = 20

  9. } else if idle > 50 { // start doubling the sleep after 1ms...

  10. delay *= 2

  11. }

  12. if delay > 10*1000 { // up to 10ms

  13. delay = 10 * 1000

  14. }

  15. usleep(delay)

  16. lastpoll := int64(atomic.Load64(&sched.lastpoll))

  17. now := nanotime()

  18. if lastpoll != 0 && lastpoll+10*1000*1000 < now {

  19. atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))

  20. gp := netpoll(false) // non-blocking - returns list of goroutines

  21. if gp != nil {

  22. ......

  23. incidlelocked(-1)

  24. // 把epoll ready的G列表注入到全局runq里

  25. injectglist(gp)

  26. incidlelocked(1)

  27. }

  28. }

  29. // retake P's blocked in syscalls

  30. // and preempt long running G's

  31. if retake(now) != 0 {

  32. idle = 0

  33. } else {

  34. idle++

  35. }

  36. ......

  37. }

  38. }

  39. func retake(now int64) uint32 {

  40. n := 0

  41. for i := int32(0); i < gomaxprocs; i++ {

  42. _p_ := allp[i] // 从所有P里面去找

  43. if _p_ == nil {

  44. continue

  45. }

  46. pd := &_p_.sysmontick

  47. s := _p_.status

  48. if s == _Psyscall {

  49. ......

  50. } else if s == _Prunning { // 针对正在运行的P

  51. // Preempt G if it's running for too long.

  52. t := int64(_p_.schedtick)

  53. if int64(pd.schedtick) != t {

  54. pd.schedtick = uint32(t)

  55. pd.schedwhen = now

  56. continue

  57. }

  58. // 如果已经超过forcePreemptNS(10ms),则抢占

  59. if pd.schedwhen+forcePreemptNS > now {

  60. continue

  61. }

  62. // 抢占P

  63. preemptone(_p_)

  64. }

  65. }

  66. return uint32(n)

  67. }

  68. func preemptone(_p_ *p) bool {

  69. mp := _p_.m.ptr()

  70. if mp == nil || mp == getg().m {

  71. return false

  72. }

  73. // 找到当前正在运行的G

  74. gp := mp.curg

  75. if gp == nil || gp == mp.g0 {

  76. return false

  77. }

  78. // 标记抢占状态

  79. gp.preempt = true

  80. // Every call in a go routine checks for stack overflow by

  81. // comparing the current stack pointer to gp->stackguard0.

  82. // Setting gp->stackguard0 to StackPreempt folds

  83. // preemption into the normal stack overflow check.

  84. // G里面的每一次调用都会比较当前栈指针与 gp->stackguard0 来检查堆栈溢出

  85. // 设置 gp->stackguard0 为 StackPreempt 来触发正常的堆栈溢出检测

  86. gp.stackguard0 = stackPreempt

  87. return true

  88. }

sysmon() 方法处于无限 for 循环,整个进程的生命周期监控着。retake()方法每次对所有的P遍历检查超过10ms的还在运行的G,如果有超过10ms的则通过 preemptone()进行抢占,但是要注意这里只把 gp.stackguard0赋值了一个 stackPreempt,并没有做让出 CPU 的操作,因此这里的抢占实质只是一个”标记“抢占。那么真正停止G执行的操作在哪里?

  1. // runtime/stack.go

  2. func newstack(ctxt unsafe.Pointer) {

  3. ......

  4. // NOTE: stackguard0 may change underfoot, if another thread

  5. // is about to try to preempt gp. Read it just once and use that same

  6. // value now and below.

  7. // 这里的逻辑是为G的抢占做的判断。

  8. // 判断是否是抢占引发栈扩张,如果 gp.stackguard0 == stackPreempt 则说明是抢占触发的栈扩张

  9. preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt

  10. ......

  11. //如果判断可以抢占, 则继续判断是否GC引起的, 如果是则对G的栈空间执行标记处理(扫描根对象)然后继续运行,

  12. //如果不是GC引起的则调用gopreempt_m函数完成抢占.

  13. if preempt {

  14. ......

  15. // 停止当前运行状态的G,最后放到全局runq里,释放M

  16. // 这里会进入schedule循环.阻塞到这里

  17. gopreempt_m(gp) // never return

  18. }

  19. ......

  20. }

 // runtime/proc.go 
  1. func goschedImpl(gp *g) {

  2. status := readgstatus(gp)

  3. if status&^_Gscan != _Grunning {

  4. dumpgstatus(gp)

  5. throw("bad g status")

  6. }

  7. casgstatus(gp, _Grunning, _Grunnable)

  8. dropg()

  9. lock(&sched.lock)

  10. globrunqput(gp)

  11. unlock(&sched.lock)

  12. schedule()

  13. }

我们都知道 Go 的调度是非抢占式的,要想实现G不被长时间,就只能主动触发抢占,而 Go 触发抢占的实际就是在栈扩张的时候,在 newstack 新创建栈空间的时候检测是否有抢占标记(也就是 gp.stackguard0是否等于 stackPreempt),如果有则通过 goschedImpl 方法再次进入到熟悉的 schedule 调度循环。

系统调用让出 CPU

我们程序都跑在系统上面,就绕不开与系统的交互。那么当我们的 Go 程序做系统调用的时候,系统的方法不确定会阻塞多久,而我们程序又不知道运行的状态该怎么办?

在 Go 中并没有直接对系统内核函数调用,而是封装了个 syscall.Syscall 方法,先看下实现:

  1. // syscall/syscall_unix.go

  2. func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

 
  1. // syscall/asm_linux_amd64.s

  2. TEXT ·Syscall(SB),NOSPLIT,$0-56

  3. CALL runtime·entersyscall(SB)

  4. MOVQ a1+8(FP), DI

  5. MOVQ a2+16(FP), SI

  6. MOVQ a3+24(FP), DX

  7. MOVQ $0, R10

  8. MOVQ $0, R8

  9. MOVQ $0, R9

  10. MOVQ trap+0(FP), AX // syscall entry

  11. SYSCALL // 进行系统调用

  12. CMPQ AX, $0xfffffffffffff001

  13. JLS ok

  14. MOVQ $-1, r1+32(FP)

  15. MOVQ $0, r2+40(FP)

  16. NEGQ AX

  17. MOVQ AX, err+48(FP)

  18. CALL runtime·exitsyscall(SB)

  19. RET

  20. ok:

  21. MOVQ AX, r1+32(FP)

  22. MOVQ DX, r2+40(FP)

  23. MOVQ $0, err+48(FP)

  24. CALL runtime·exitsyscall(SB)

  25.   RET

在汇编代码中看出先是执行了 runtime·entersyscall 方法,然后进行系统调用,最后执行了 runtime·exitsyscall(SB),从字面意思看是进入系统调用之前先执行一些逻辑,退出系统调用之后执行一堆逻辑。看下具体实现:

  1. // runtime/proc.go

  2. func entersyscall(dummy int32) {

  3. reentersyscall(getcallerpc(unsafe.Pointer(&dummy)), getcallersp(unsafe.Pointer(&dummy)))

  4. }

  5. func reentersyscall(pc, sp uintptr) {

  6. ......

  7. // Leave SP around for GC and traceback.

  8. // 保存执行现场

  9. save(pc, sp)

  10. _g_.syscallsp = sp

  11. _g_.syscallpc = pc

  12. // 切换到系统调用状态

  13. casgstatus(_g_, _Grunning, _Gsyscall)

  14. ......

  15. // Goroutines must not split stacks in Gsyscall status (it would corrupt g->sched).

  16. // We set _StackGuard to StackPreempt so that first split stack check calls morestack.

  17. // Morestack detects this case and throws.

  18. _g_.stackguard0 = stackPreempt

  19. _g_.m.locks--

  20. }

进入系统调用前先保存执行现场,然后切换到_Gsyscall 状态,最后标记抢占,等待被抢占走。

  1. // runtime/proc.go

  2. func exitsyscall(dummy int32) {

  3. ......

  4. // Call the scheduler.

  5. mcall(exitsyscall0)

  6. ......

  7. }

  8. func exitsyscall0(gp *g) {

  9. _g_ := getg()

  10. casgstatus(gp, _Gsyscall, _Grunnable)

  11. dropg()

  12. lock(&sched.lock)

  13. // 获取一个空闲的P,如果没有则放到全局队列里,如果有则执行

  14. _p_ := pidleget()

  15. if _p_ == nil {

  16. globrunqput(gp) // 如果没有P就放到全局队列里,等待有资源时执行

  17. } else if atomic.Load(&sched.sysmonwait) != 0 {

  18. atomic.Store(&sched.sysmonwait, 0)

  19. notewakeup(&sched.sysmonnote)

  20. }

  21. unlock(&sched.lock)

  22. if _p_ != nil {

  23. acquirep(_p_)

  24. execute(gp, false) // Never returns. // 如果找到空闲的P则直接执行

  25. }

  26. if _g_.m.lockedg != nil {

  27. // Wait until another thread schedules gp and so m again.

  28. stoplockedm()

  29. execute(gp, false) // Never returns.

  30. }

  31. stopm()

  32. schedule() // Never returns. // 没有P资源执行,就继续下一轮调度循环

  33. }

系统调用退出时,切到 G0 下把G状态切回来,如果有可执行的P则直接执行,如果没有则放到全局队列里,等待调度,最后又看到了熟悉的 schedule 进入下一轮调度循环。

待执行G的来源

gofunc 创建G

当开启一个 Goroutine 的时候用到 go func()这样的语法,在 runtime 下其实调用的就是 newproc 方法。

  1. // runtime/proc.go

  2. func newproc(siz int32, fn *funcval) {

  3. argp := add(unsafe.Pointer(&fn), sys.PtrSize)

  4. pc := getcallerpc(unsafe.Pointer(&siz))

  5. systemstack(func() {

  6. newproc1(fn, (*uint8)(argp), siz, 0, pc)

  7. })

  8. }

  9. func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {

  10. ......

  11. _p_ := _g_.m.p.ptr()

  12. // 从当前P里面复用一个空闲G

  13. newg := gfget(_p_)

  14. // 如果没有空闲G则新建一个,默认堆大小为_StackMin=2048 bytes

  15. if newg == nil {

  16. newg = malg(_StackMin)

  17. casgstatus(newg, _Gidle, _Gdead)

  18. // 把新创建的G添加到全局allg里

  19. allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.

  20. }

  21. ......

  22. if isSystemGoroutine(newg) {

  23. atomic.Xadd(&sched.ngsys, +1)

  24. }

  25. newg.gcscanvalid = false

  26. casgstatus(newg, _Gdead, _Grunnable)

  27. // 把G放到P里的待运行队列,第三参数设置为true,表示要放到runnext里,作为优先要执行的G

  28. runqput(_p_, newg, true)

  29. // 如果有其它空闲P则尝试唤醒某个M来执行

  30. // 如果有M处于自璇等待P或G状态,放弃。

  31. // NOTE: sched.nmspinning!=0说明正在有M被唤醒,这里判断sched.nmspinnin==0时才进入wakep是防止同时唤醒多个M

  32. if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {

  33. wakep()

  34. }

  35. ......

  36. return newg

  37. }

newproc1方法中 gfget 先从空闲的G列表获取一个G对象,没有则创建一个新的G对象,然后 runqput 放到当前P待运行队列里。

epoll 来源

回想上面分析抢占以及多线程下如何调度时都见到一个 netpoll 方法,这个方法就是从系统内核获取已经有数据的时间,然后映射到对应的G标记 ready。下面看实现:

  1. // runtime/proc.go

  2. func netpoll(block bool) *g {

  3. ......

  4. var events [128]epollevent

  5. retry:

  6. n := epollwait(epfd, &events[0], int32(len(events)), waitms)

  7. if n < 0 {

  8. if n != -_EINTR {

  9. println("runtime: epollwait on fd", epfd, "failed with", -n)

  10. throw("runtime: netpoll failed")

  11. }

  12. goto retry

  13. }

  14. var gp guintptr

  15. for i := int32(0); i < n; i++ {

  16. ev := &events[i]

  17. if ev.events == 0 {

  18. continue

  19. }

  20. var mode int32

  21. if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {

  22. mode += 'r'

  23. }

  24. if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {

  25. mode += 'w'

  26. }

  27. if mode != 0 {

  28. pd := *(**pollDesc)(unsafe.Pointer(&ev.data))

  29. netpollready(&gp, pd, mode)

  30. }

  31. }

  32. if block && gp == 0 {

  33. goto retry

  34. }

  35. return gp.ptr()

  36. }

  37. func netpollready(gpp *guintptr, pd *pollDesc, mode int32) {

  38. var rg, wg guintptr

  39. if mode == 'r' || mode == 'r'+'w' {

  40. rg.set(netpollunblock(pd, 'r', true))

  41. }

  42. if mode == 'w' || mode == 'r'+'w' {

  43. wg.set(netpollunblock(pd, 'w', true))

  44. }

  45. if rg != 0 {

  46. rg.ptr().schedlink = *gpp

  47. *gpp = rg

  48. }

  49. if wg != 0 {

  50. wg.ptr().schedlink = *gpp

  51. *gpp = wg

  52. }

  53. }

  54. // 解锁pd wait状态,标记为pdReady,并返回

  55. func netpollunblock(pd *pollDesc, mode int32, ioready bool) *g {

  56. gpp := &pd.rg

  57. if mode == 'w' {

  58. gpp = &pd.wg

  59. }

  60. for {

  61. old := *gpp

  62. if old == pdReady {

  63. return nil

  64. }

  65. if old == 0 && !ioready {

  66. // Only set READY for ioready. runtime_pollWait

  67. // will check for timeout/cancel before waiting.

  68. return nil

  69. }

  70. var new uintptr

  71. if ioready {

  72. new = pdReady

  73. }

  74. // 变量pd.rg在netpollblock的时候已经指向了运行pd的G,因此old其实指向G的指针,而不是pdWait等等的状态指针了

  75. if atomic.Casuintptr(gpp, old, new) {

  76. if old == pdReady || old == pdWait {

  77. old = 0

  78. }

  79. return (*g)(unsafe.Pointer(old))

  80. }

  81. }

  82. }

首先 epollwait 从内核获取到一批 event,也就拿到了有收到就绪的 FD。netpoll 的返回值是一个G链表,在该方法里只是把要被唤醒的G标记 ready,然后交给外部处理,例如 sysmon 中的代码:

// runtime/proc.go

  1. func sysmon() {

  2. ......

  3. for {

  4. ......

  5. lastpoll := int64(atomic.Load64(&sched.lastpoll))

  6. now := nanotime()

  7. if lastpoll != 0 && lastpoll+10*1000*1000 < now {

  8. atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))

  9. gp := netpoll(false) // non-blocking - returns list of goroutines

  10. if gp != nil {

  11. ......

  12. incidlelocked(-1)

  13. // 把epoll ready的G列表注入到全局runq里

  14. injectglist(gp)

  15. incidlelocked(1)

  16. }

  17. }

  18. ......

  19. }

  20. }

  21. // 把G列表注入到全局runq里

  22. func injectglist(glist *g) {

  23. ......

  24. lock(&sched.lock)

  25. var n int

  26. for n = 0; glist != nil; n++ {

  27. gp := glist

  28. glist = gp.schedlink.ptr()

  29. casgstatus(gp, _Gwaiting, _Grunnable)

  30. globrunqput(gp)

  31. }

  32. ......

  33. }

netpoll 返回的链表交给了 injectglist,然后其实是放到了全局 rung 队列中,等待被调度。

epoll 内容较多,本章主要围绕调度的话题讨论,在这里就不展开分析。

看几个主动让出 CPU 的场景

time.Sleep

当代码中调用 time.Sleep 的时候我们是要 black 住程序不在继续往下执行,此时该 goroutine 不会做其他事情了,理应把 CPU 资源释放出来,下面看下实现:

  1. // runtime/time.go

  2. func timeSleep(ns int64) {

  3. if ns <= 0 {

  4. return

  5. }

  6. t := getg().timer

  7. if t == nil {

  8. t = new(timer)

  9. getg().timer = t

  10. }

  11. *t = timer{} // 每个定时任务都创建一个timer

  12. t.when = nanotime() + ns

  13. t.f = goroutineReady // 记录唤醒该G的方法,唤醒时通过该方法执行唤醒

  14. t.arg = getg() // 把timer与当前G关联,时间到了唤醒时通过该参数找到所在的G

  15. lock(&timers.lock)

  16. addtimerLocked(t) // 把timer添加到最小堆里

  17. goparkunlock(&timers.lock, "sleep", traceEvGoSleep, 2) // 切到G0让出CPU,进入休眠

  18. }

  1. // runtime/proc.go

  2. func goparkunlock(lock *mutex, reason string, traceEv byte, traceskip int) {

  3. gopark(parkunlock_c, unsafe.Pointer(lock), reason, traceEv, traceskip)

  4. }

timeSleep 函数里通过 addtimerLocked 把定时器加入到 timer 管理器(timer 通过最小堆的数据结构存放每个定时器,在这不做详细说明)后,再通过 goparkunlock 实现把当前G休眠,这里看到了上面提到的 gopark 方法进行调度循环的上下文切换。

上面介绍的是一个G如何进入到休眠状态的过程,该例子是个定时器,当时间到了的话,当前G就要被唤醒继续执行了。下面就介绍下唤醒的流程。

返回到最开始 timeSleep 方法里在进入调度方法之前有一个 addtimerLocked 方法,看下这个方法做了什么。

  1. // runtime/time.go

  2. func addtimerLocked(t *timer) {

  3. // when must never be negative; otherwise timerproc will overflow

  4. // during its delta calculation and never expire other runtime timers.

  5. if t.when < 0 {

  6. t.when = 1<<63 - 1

  7. }

  8. t.i = len(timers.t)

  9. timers.t = append(timers.t, t) //将当前timer添加到timer管理器里

  10. siftupTimer(t.i)

  11. ......

  12. // 如果没有启动timer管理定时器,则启动。timerproc只会启动一次,即全局timer管理器

  13. if !timers.created {

  14. timers.created = true

  15. go timerproc()

  16. }

  17. }

  1. // runtime/time.go

  2. // Timerproc runs the time-driven events.

  3. // It sleeps until the next event in the timers heap.

  4. // If addtimer inserts a new earlier event, it wakes timerproc early.

  5. func timerproc() {

  6. timers.gp = getg()

  7. for {

  8. lock(&timers.lock)

  9. timers.sleeping = false

  10. now := nanotime()

  11. delta := int64(-1)

  12. for {

  13. if len(timers.t) == 0 {

  14. delta = -1

  15. break

  16. }

  17. t := timers.t[0]

  18. delta = t.when - now

  19. if delta > 0 {

  20. break

  21. }

  22. if t.period > 0 {

  23. // leave in heap but adjust next time to fire

  24. t.when += t.period * (1 + -delta/t.period)

  25. siftdownTimer(0)

  26. } else {

  27. // remove from heap

  28. last := len(timers.t) - 1

  29. if last > 0 {

  30. timers.t[0] = timers.t[last]

  31. timers.t[0].i = 0

  32. }

  33. timers.t[last] = nil

  34. timers.t = timers.t[:last]

  35. if last > 0 {

  36. siftdownTimer(0)

  37. }

  38. t.i = -1 // mark as removed

  39. }

  40. f := t.f

  41. arg := t.arg

  42. seq := t.seq

  43. unlock(&timers.lock)

  44. if raceenabled {

  45. raceacquire(unsafe.Pointer(t))

  46. }

  47. f(arg, seq)

  48. lock(&timers.lock)

  49. }

  50. ......

  51. }

  52. }

在 addtimerLocked 方法的最下面有个逻辑在运行期间开启了'全局时间事件驱动器'timerproc,该方法会全程遍历最小堆,寻找最早进入 timer 管理器的定时器,然后唤醒。他是怎么找到要唤醒哪个G的?回头看下 timeSleep 方法里把当时正在执行的G以及唤醒方法 goroutineReady 带到了每个定时器里,而在 timerproc 则通过找到期的定时器执行f(arg, seq)

即通过 goroutineReady 方法唤醒。方法调用过程: goroutineReady() -> ready()

 
 
  1. /// runtime/time.go

  2. func goroutineReady(arg interface{}, seq uintptr) {

  3. goready(arg.(*g), 0)

  4. }

 
 
  1. // runtime/proc.go

  2. func goready(gp *g, traceskip int) {

  3. systemstack(func() {

  4. ready(gp, traceskip, true)

  5. })

  6. }

  7. // Mark gp ready to run.

  8. func ready(gp *g, traceskip int, next bool) {

  9. if trace.enabled {

  10. traceGoUnpark(gp, traceskip)

  11. }

  12. status := readgstatus(gp)

  13. // Mark runnable.

  14. _g_ := getg()

  15. _g_.m.locks++ // disable preemption because it can be holding p in a local var

  16. if status&^_Gscan != _Gwaiting {

  17. dumpgstatus(gp)

  18. throw("bad g->status in ready")

  19. }

  20. // status is Gwaiting or Gscanwaiting, make Grunnable and put on runq

  21. casgstatus(gp, _Gwaiting, _Grunnable)

  22. runqput(_g_.m.p.ptr(), gp, next)

  23. ......

  24. }

在上面的方法里可以看到先把休眠的G从_Gwaiting 切换到_Grunnable 状态,表明已经可运行。然后通过 runqput 方法把G放到P的待运行队列里,就进入到调度器的调度循环里了。

总结:time.Sleep 想要进入阻塞(休眠)状态,其实是通过 gopark 方法给自己标记个_Gwaiting 状态,然后把自己所占用的CPU线程资源给释放出来,继续执行调度任务,调度其它的G来运行。而唤醒是通过把G更改回_Grunnable 状态后,然后把G放入到P的待运行队列里等待执行。通过这点还可以看出休眠中的G其实并不占用 CPU 资源,最多是占用内存,是个很轻量级的阻塞。

Mutex

 
 
  1. // sync/mutex.go

  2. func (m *Mutex) Lock() {

  3. // Fast path: grab unlocked mutex.

  4. // 首先尝试抢锁,如果抢到则直接返回,并标记mutexLocked状态

  5. if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {

  6. if race.Enabled {

  7. race.Acquire(unsafe.Pointer(m))

  8. }

  9. return

  10. }

  11. var waitStartTime int64

  12. starving := false

  13. awoke := false

  14. iter := 0

  15. old := m.state

  16. for {

  17. // Don't spin in starvation mode, ownership is handed off to waiters

  18. // so we won't be able to acquire the mutex anyway.

  19. // 尝试自璇,但有如下几个条件跳过自璇,这里的自璇是用户态自璇,基本lock的cpu消耗都耗到这里了

  20. // 1.不在饥饿模式自璇

  21. // 2.超过4次循环,则不再自璇. (runtime_canSpin里面)

  22. // 3.全部P空闲时,不自璇.(runtime_canSpin里面)

  23. // 4.当前P里无运行G时,不自璇.(runtime_canSpin里面)

  24. if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {

  25. // Active spinning makes sense.

  26. // Try to set mutexWoken flag to inform Unlock

  27. // to not wake other blocked goroutines.

  28. if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&

  29. atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {

  30. awoke = true

  31. }

  32. runtime_doSpin() // doSpin其实就是用户态自璇30次

  33. iter++

  34. old = m.state

  35. continue

  36. }

  37. ......

  38. if atomic.CompareAndSwapInt32(&m.state, old, new) {

  39. ......

  40. runtime_SemacquireMutex(&m.sema, queueLifo) // 这里会再次自璇几次,然后最后切换到g0把G标记_Gwaiting状态阻塞在这里

  41. starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs // 如果锁等了1毫秒才被唤醒,才会标记为饥饿模式

  42. old = m.state

  43. ......

  44. } else {

  45. old = m.state

  46. }

  47. }

  48. if race.Enabled {

  49. race.Acquire(unsafe.Pointer(m))

  50. }

  51. }

 
 
  1. // runtime/sema.go

  2. func sync_runtime_Semacquire(addr *uint32) {

  3. semacquire1(addr, false, semaBlockProfile)

  4. }

  5. func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags) {

  6. ......

  7. for {

  8. ......

  9. // Any semrelease after the cansemacquire knows we're waiting

  10. // (we set nwait above), so go to sleep.

  11. root.queue(addr, s, lifo) // 把当前锁的信息存起来以便以后唤醒时找到当前G,G是在queue里面获取的。

  12. goparkunlock(&root.lock, "semacquire", traceEvGoBlockSync, 4) // 进行休眠,然后阻塞在这里

  13. if s.ticket != 0 || cansemacquire(addr) {

  14. break

  15. }

  16. }

  17. }

  18. // queue adds s to the blocked goroutines in semaRoot.

  19. func (root *semaRoot) queue(addr *uint32, s *sudog, lifo bool) {

  20. s.g = getg() // 这里记录了当前的G,以便唤醒的时候找到要被唤醒的G

  21. s.elem = unsafe.Pointer(addr)

  22. s.next = nil

  23. s.prev = nil

  24. var last *sudog

  25. pt := &root.treap

  26. for t := *pt; t != nil; t = *pt {

  27. ......

  28. last = t

  29. if uintptr(unsafe.Pointer(addr)) < uintptr(t.elem) {

  30. pt = &t.prev

  31. } else {

  32. pt = &t.next

  33. }

  34. }

  35.   ......

Mutex.Lock 方法通过调用 runtime_SemacquireMutex 最终还是调用 goparkunlock 实现把G进入到休眠状态。在进入休眠之前先把自己加入到队列里 root.queue(addr, s, lifo),在 queue 方法里,记录了当前的G,以便以后找到并唤醒。

 
 
  1. // sync/mutex.go

  2. func (m *Mutex) Unlock() {

  3. ......

  4. if new&mutexStarving == 0 { // 如果不是饥饿模式

  5. old := new

  6. for {

  7. ......

  8. if atomic.CompareAndSwapInt32(&m.state, old, new) {

  9. runtime_Semrelease(&m.sema, false) // 唤醒锁

  10. return

  11. }

  12. old = m.state

  13. }

  14. } else {

  15. // Starving mode: handoff mutex ownership to the next waiter.

  16. // Note: mutexLocked is not set, the waiter will set it after wakeup.

  17. // But mutex is still considered locked if mutexStarving is set,

  18. // so new coming goroutines won't acquire it.

  19. runtime_Semrelease(&m.sema, true) // 唤醒锁

  20. }

  21. }

 
 
  1. // runtime/sema.go

  2. func sync_runtime_Semrelease(addr *uint32, handoff bool) {

  3. semrelease1(addr, handoff)

  4. }

  5. func semrelease1(addr *uint32, handoff bool) {

  6. root := semroot(addr)

  7. s, t0 := root.dequeue(addr)

  8. if s != nil {

  9. atomic.Xadd(&root.nwait, -1)

  10. }

  11. ......

  12. if s != nil { // May be slow, so unlock first

  13. ......

  14. readyWithTime(s, 5)

  15. }

  16. }

  17. func readyWithTime(s *sudog, traceskip int) {

  18. if s.releasetime != 0 {

  19. s.releasetime = cputicks()

  20. }

  21. goready(s.g, traceskip)

  22. }

Mutex. Unlock 方法通过调用 runtime_Semrelease 最终还是调用 goready 实现把G唤醒。

channel

 
 
  1. // runtime/chan.go

  2. func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {

  3. // 寻找一个等待中的receiver,直接把值传给这个receiver,绕过下面channel buffer,

  4. // 避免从sender buffer->chan buffer->receiver buffer,而是直接sender buffer->receiver buffer,仍然做了内存copy

  5. if sg := c.recvq.dequeue(); sg != nil {

  6. send(c, sg, ep, func() { unlock(&c.lock) }, 3)

  7. return true

  8. }

  9. // 如果没有receiver等待:

  10. // 如果当前chan里的元素个数小于环形队列大小(也就是chan还没满),则把内存拷贝到channel buffer里,然后直接返回。

  11. // 注意dataqsiz是允许为0的,当为0时,也不存在该if里面的内存copy

  12. if c.qcount < c.dataqsiz {

  13. // Space is available in the channel buffer. Enqueue the element to send.

  14. qp := chanbuf(c, c.sendx) // 获取即将要写入的chan buffer的指针地址

  15. if raceenabled {

  16. raceacquire(qp)

  17. racerelease(qp)

  18. }

  19. // 把元素内存拷贝进去.

  20. // 注意这里产生了一次内存copy,也就是说如果没有receiver的话,就一定会产生内存拷贝

  21. typedmemmove(c.elemtype, qp, ep)

  22. c.sendx++ // 发送索引+1

  23. if c.sendx == c.dataqsiz {

  24. c.sendx = 0

  25. }

  26. c.qcount++ // 队列元素计数器+1

  27. unlock(&c.lock)

  28. return true

  29. }

  30. if !block { // 如果是非阻塞的,到这里就可以结束了

  31. unlock(&c.lock)

  32. return false

  33. }

  34. // ########下面是进入阻塞模式的如何实现阻塞的处理逻辑

  35. // Block on the channel. Some receiver will complete our operation for us.

  36. // 把元素相关信息、当前的G信息打包到一个sudog里,然后扔进send队列

  37. gp := getg()

  38. mysg := acquireSudog()

  39. mysg.releasetime = 0

  40. if t0 != 0 {

  41. mysg.releasetime = -1

  42. }

  43. // No stack splits between assigning elem and enqueuing mysg

  44. // on gp.waiting where copystack can find it.

  45. mysg.elem = ep

  46. mysg.waitlink = nil

  47. mysg.g = gp // 把当前G也扔进sudog里,用于别人唤醒该G的时候找到该G

  48. mysg.selectdone = nil

  49. mysg.c = c

  50. gp.waiting = mysg // 记录当前G正在等待的sudog

  51. gp.param = nil

  52. c.sendq.enqueue(mysg)

  53. // 切换到g0,把当前G切换到_Gwaiting状态,然后唤醒lock.

  54. // 此时当前G被阻塞了,P就继续执行其它G去了.

  55. goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)

  56. ......

  57. return true

  58. }

  59. func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {

  60. ......

  61. gp := sg.g

  62. unlockf()

  63. gp.param = unsafe.Pointer(sg)

  64. if sg.releasetime != 0 {

  65. sg.releasetime = cputicks()

  66. }

  67. goready(gp, skip+1)

  68. }

当给一个 chan 发送消息的时候,实质触发的方法是 chansend。在该方法里不是先进入休眠状态。

1)如果此时有接收者接收这个 chan 的消息则直接把数据通过 send 方法扔给接收者,并唤醒接收者的G,然后当前G则继续执行。

2)如果没有接收者,就把数据 copy 到 chan 的临时内存里,且内存没有满就继续执行当前G。

3)如果没有接收者且 chan 满了,依然是通过 goparkunlock 方法进入休眠。在休眠前把当前的G相关信息存到队列(sendq)以便有接收者接收数据的时候唤醒当前G。

 
 
  1. func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {

  2. ......

  3. if sg := c.sendq.dequeue(); sg != nil {

  4. // Found a waiting sender. If buffer is size 0, receive value

  5. // directly from sender. Otherwise, receive from head of queue

  6. // and add sender's value to the tail of the queue (both map to

  7. // the same buffer slot because the queue is full).

  8. // 寻找一个正在等待的sender

  9. // 如果buffer size是0,则尝试直接从sender获取(这种情况是在环形队列长度(dataqsiz)为0的时候出现)

  10. // 否则(buffer full的时候)从队列head接收,并且帮助sender在队列满时的阻塞的元素信息拷贝到队列里,然后将sender的G状态切换为_Grunning,这样sender就不阻塞了。

  11. recv(c, sg, ep, func() { unlock(&c.lock) }, 3)

  12. return true, true

  13. }

  14. // 如果有数据则从channel buffer里获取数据后返回(此时环形队列长度dataqsiz!=0)

  15. if c.qcount > 0 {

  16. // Receive directly from queue

  17. qp := chanbuf(c, c.recvx) // 获取即将要读取的chan buffer的指针地址

  18. if raceenabled {

  19. raceacquire(qp)

  20. racerelease(qp)

  21. }

  22. if ep != nil {

  23. typedmemmove(c.elemtype, ep, qp) // copy元素数据内存到channel buffer

  24. }

  25. typedmemclr(c.elemtype, qp)

  26. c.recvx++

  27. if c.recvx == c.dataqsiz {

  28. c.recvx = 0

  29. }

  30. c.qcount--

  31. unlock(&c.lock)

  32. return true, true

  33. }

  34. if !block {

  35. unlock(&c.lock)

  36. return false, false

  37. }

  38. // ##########下面是无任何数据准备把当前G切换为_Gwaiting状态的逻辑

  39. // no sender available: block on this channel.

  40. gp := getg()

  41. mysg := acquireSudog()

  42. mysg.releasetime = 0

  43. if t0 != 0 {

  44. mysg.releasetime = -1

  45. }

  46. // No stack splits between assigning elem and enqueuing mysg

  47. // on gp.waiting where copystack can find it.

  48. mysg.elem = ep

  49. mysg.waitlink = nil

  50. gp.waiting = mysg

  51. mysg.g = gp

  52. mysg.selectdone = nil

  53. mysg.c = c

  54. gp.param = nil

  55. c.recvq.enqueue(mysg)

  56. // 释放了锁,然后把当前G切换为_Gwaiting状态,阻塞在这里等待有数据进来被唤醒

  57. goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)

  58. ......

  59. return true, !closed

  60. }

  61. func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {

  62. ......

  63. sg.elem = nil

  64. gp := sg.g

  65. unlockf()

  66. gp.param = unsafe.Pointer(sg)

  67. if sg.releasetime != 0 {

  68. sg.releasetime = cputicks()

  69. }

  70. goready(gp, skip+1)

  71. }

chanrecv 方法是在 chan 接收者的地方调用的方法。

1)如果有发送者被休眠,则取出数据然后唤醒发送者,当前接收者的G拿到数据继续执行。

2)如果没有等待的发送者就看下有没有发送的数据还没被接收,有的话就直接取出数据然后返回,当前接收者的G拿到数据继续执行。(注意:这里取的数据不是正在等待的 sender 的数据,而是从 chan 的开头的内存取,如果是 sender 的数据则读出来的数据顺序就乱了)

3)如果即没有发送者,chan 里也没数据就通过 goparkunlock 进行休眠,在休眠之前把当前的G相关信息存到 recvq 里面,以便有数据时找到要唤醒的G。

END

从源码角度看 Golang 的调度相关推荐

  1. 从源码角度看Android系统SystemServer进程启动过程

    SystemServer进程是由Zygote进程fork生成,进程名为system_server,主要用于创建系统服务. 备注:本文将结合Android8.0的源码看SystemServer进程的启动 ...

  2. 从JDK源码角度看Long

    概况 Java的Long类主要的作用就是对基本类型long进行封装,提供了一些处理long类型的方法,比如long到String类型的转换方法或String类型到long类型的转换方法,当然也包含与其 ...

  3. 从源码角度看Android系统Launcher在开机时的启动过程

    Launcher是Android所有应用的入口,用来显示系统中已经安装的应用程序图标. Launcher本身也是一个App,一个提供桌面显示的App,但它与普通App有如下不同: Launcher是所 ...

  4. 从源码角度看Android系统Zygote进程启动过程

    在Android系统中,DVM.ART.应用程序进程和SystemServer进程都是由Zygote进程创建的,因此Zygote又称为"孵化器".它是通过fork的形式来创建应用程 ...

  5. 从JDK源码角度看Short

    概况 Java的Short类主要的作用就是对基本类型short进行封装,提供了一些处理short类型的方法,比如short到String类型的转换方法或String类型到short类型的转换方法,当然 ...

  6. 从源码角度看CPU相关日志

    简介 (本文原地址在我的博客CheapTalks, 欢迎大家来看看~) 安卓系统中,普通开发者常常遇到的是ANR(Application Not Responding)问题,即应用主线程没有相应.根本 ...

  7. 从template到DOM(Vue.js源码角度看内部运行机制)

    写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(github.com/answershuto-)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些产出也会对同样想要学习Vue.j ...

  8. 从源码角度看Spark on yarn client cluster模式的本质区别

    首先区分下AppMaster和Driver,任何一个yarn上运行的任务都必须有一个AppMaster,而任何一个Spark任务都会有一个Driver,Driver就是运行SparkContext(它 ...

  9. hotspot源码角度看OOP之类属性的底层实现(一)

    hello,大家好,我是江湖人送外号[道格牙]的子牙老师. 最近看hotspo源码有点入迷.hotspot就像一座宝库,等你探索的东西太多了.每次达到一个新的Level回头细看,都有不同的感触.入迷归 ...

最新文章

  1. cramer定理_Lundberg-Cramer定理
  2. python列表输入不加逗号_用python打印不带括号或逗号的列表
  3. (天国之扉文章抢救) 1/10/2003 总结?总结!
  4. 2019年安徽省模块七满分多少_艺考资讯 | 2021年美术统考考多少分才能通过?过了合格线有什么意义?美术生一定要重视!...
  5. Linux命令操作,文件复制,删除修改等
  6. ibatis的isequal_isequal ibatis
  7. c#精彩编程200例百度云_邂逅百度云智学院:福州理工学院AIOT实训营火热开营!...
  8. forge开发_使用Forge,WildFly Swarm和Arquillian开发微服务
  9. JVM垃圾回收机制学习
  10. Qt 字符串QString arg()用法总结
  11. 《奠基计算机网络》2011年8月15日出版 视频下载 http://www.91xueit.com
  12. Android 系统(42)---Android7.0 PowerManagerService亮灭屏分析(三)
  13. 万能淘口令生成api,淘口令转化api,淘口令万能版api,淘口令生成器api
  14. Chrome谷歌浏览器Flash Player被屏蔽如何解决
  15. Bootstrap文字排版
  16. 细胞穿膜肽-MnO2复合物(TAT-MnO2)多肽偶联氧化锰纳米粒|MnO2包裹聚多巴胺的纳米颗粒
  17. 可视化拖拽组件库一些技术要点原理分析(三)
  18. MySQL安装一直卡在starting server
  19. 川土微电子产品在PLC/伺服领域的应用
  20. Hank的无线802.11学习笔记--2

热门文章

  1. Linux环境下PGI编译器pgf90的安装
  2. 订单失效怎么做的_此招一出,数据库压力降低90%,携程机票订单缓存系统实践...
  3. 阿里巴巴java开发手册(2020版)
  4. maven项目需要提交到版本库管理的文件
  5. 豪华钟表江诗丹顿将使用区块链溯源
  6. T-Bootstrap-day03-弹性布局、表单、常见组件
  7. gis等时圈怎么做_【干货分享】如何一键生成等时圈?
  8. 【高级软考】专业术语详解
  9. 写一个批量制作散点图并导出的matlab程序
  10. java知识体系整理(二)JVM、GC回收及调优