go中goroutine初探

柯基是只dog · · 909 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

go func...会被编译成newproc方法,我们可以随便写一个测试文件编译后用gdb打开

(gdb) b runtime.newproc
Breakpoint 5 at 0x10303c0: file /usr/local/homebrew/Cellar/go@1.9/1.9.6/libexec/src/runtime/proc.go, line 2929.

func newproc(siz int32, fn *funcval) {
    argp := add(unsafe.Pointer(&fn), sys.PtrSize)
    pc := getcallerpc(unsafe.Pointer(&siz))
    systemstack(func() {
        newproc1(fn, (*uint8)(argp), siz, 0, pc)
    })
}

type funcval struct {
    fn uintptr
    // variable-size, fn-specific data here
}

golang的参数调用方式和C差不多,都是按参数从右到左入栈,所以siz是第一个参数,fn是变长参数,它的长度由siz确定,所以从我们的代码 go func...最终会被编译器call newproc,并且会把参数拷贝到调用栈上,fn中包含了所有g执行的上下文(方法指针[IP]和方法参数)

还一个有意思的是取调用方的pc,汇编中call调用后cpu会把下一条地址也就是方法的返回地址压入栈中。

systemstack是让M使用g0堆栈,我们已经知道M执行的时候会从G中恢复出堆栈执行,但做一些系统任务比如这儿的新建proc的时候如果再用G的堆栈就不合适了,所以golang设计的时候每个M都分配一个g0堆栈,这一点我觉得应该是模仿操作系统的,cpu有4个等级,linux使用0和3,用户态在3级,当系统调用陷入内核的时候操作系统会切换到0级并且把堆栈都切换到内核堆栈。

继续newproc1

//  返回值忽略了,因为goroutine从不返回
func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {
    _g_ := getg()

    if fn == nil {
        _g_.m.throwing = -1 // do not dump full stacks
        throw("go of nil func value")
    }
    _g_.m.locks++ // disable preemption because it can be holding p in a local var
    // 对其内存
    siz := narg + nret
    siz = (siz + 7) &^ 7

    // We could allocate a larger initial stack if necessary.
    // Not worth it: this is almost always an error.
    // 4*sizeof(uintreg): extra space added below
    // sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).
    if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
        throw("newproc: function arguments too large for new goroutine")
    }

    // 从P获取一个G如果没有就new一个
    _p_ := _g_.m.p.ptr()
    newg := gfget(_p_)
    if newg == nil {
        newg = malg(_StackMin)
        casgstatus(newg, _Gidle, _Gdead)
        allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
    }
    if newg.stack.hi == 0 {
        throw("newproc1: newg missing stack")
    }

    if readgstatus(newg) != _Gdead {
        throw("newproc1: new g is not Gdead")
    }

    totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
    totalSize += -totalSize & (sys.SpAlign - 1)                  // align to spAlign
    sp := newg.stack.hi - totalSize
    spArg := sp
    if usesLR {
        // caller's LR
        *(*uintptr)(unsafe.Pointer(sp)) = 0
        prepGoExitFrame(sp)
        spArg += sys.MinFrameSize
    }
    if narg > 0 {
        // 把参数copy到G的栈中 
        memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
        // This is a stack-to-stack copy. If write barriers
        // are enabled and the source stack is grey (the
        // destination is always black), then perform a
        // barrier copy. We do this *after* the memmove
        // because the destination stack may have garbage on
        // it.
        if writeBarrier.needed && !_g_.m.curg.gcscandone {
            f := findfunc(fn.fn)
            stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
            // We're in the prologue, so it's always stack map index 0.
            bv := stackmapdata(stkmap, 0)
            bulkBarrierBitmap(spArg, spArg, uintptr(narg), 0, bv.bytedata)
        }
    }

    memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
    newg.sched.sp = sp
    newg.stktopsp = sp
    newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
    newg.sched.g = guintptr(unsafe.Pointer(newg))
    // 该方法会把pc换成fn.fn也就是方法执行的地址,而把老pc放入lr,但我不太懂为什么要这么写
    gostartcallfn(&newg.sched, fn)
    newg.gopc = callerpc
    newg.startpc = fn.fn
    if _g_.m.curg != nil {
        newg.labels = _g_.m.curg.labels
    }
    if isSystemGoroutine(newg) {
        atomic.Xadd(&sched.ngsys, +1)
    }
    newg.gcscanvalid = false
    casgstatus(newg, _Gdead, _Grunnable)

    if _p_.goidcache == _p_.goidcacheend {
        // Sched.goidgen is the last allocated id,
        // this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
        // At startup sched.goidgen=0, so main goroutine receives goid=1.
        _p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
        _p_.goidcache -= _GoidCacheBatch - 1
        _p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
    }
    newg.goid = int64(_p_.goidcache)
    _p_.goidcache++
    if raceenabled {
        newg.racectx = racegostart(callerpc)
    }
    if trace.enabled {
        traceGoCreate(newg, newg.startpc)
    }
    // 好了G的创建完成了,放入到P的run队列中
    runqput(_p_, newg, true)

  // 如果有空闲P就尝试唤醒一个M
    if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
        wakep()
    }
    _g_.m.locks--
    if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack
        _g_.stackguard0 = stackPreempt
    }
    return newg
}

// 这就是上面说的把pc替换成真正的fn.fn的地方,不懂为什么要这么写
func gostartcallfn(gobuf *gobuf, fv *funcval) {
    var fn unsafe.Pointer
    if fv != nil {
        fn = unsafe.Pointer(fv.fn)
    } else {
        fn = unsafe.Pointer(funcPC(nilfunc))
    }
    gostartcall(gobuf, fn, unsafe.Pointer(fv))
}

func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
    if buf.lr != 0 {
        throw("invalid use of gostartcall")
    }
    buf.lr = buf.pc
    buf.pc = uintptr(fn)
    buf.ctxt = ctxt
}

总结下go在go func中干了啥

  • 编译器会转成newproc
  • newproc中从调用栈中取出执行方法地址,调用参数,调用者pc
  • 然后从P中取一个空闲G没有就创建一个newG
  • 对newG赋值,主要是赋值G的sched,它保存了G的调用上下文,有方法地址,参数,栈地址,下次M从P中取到这个G的时候运行就会从上下文中恢复到寄存器中从而完成用户态进程的切换
  • 最后结束的时候会尝试唤醒M

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:柯基是只dog

查看原文:go中goroutine初探

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

909 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传