深入理解GO语言之并发机制

前言:可以说GO真正吸引到我的就是并发这块了,深入理解这个机制后让我收益匪浅,接下来就用自己薄弱的认知来谈谈GO的并发机制。

一,初始化过程

在这之前,先看下asm_arm64.s中的汇编代码关于启动这块的逻辑

CALL    runtime·args(SB)
CALL    runtime·osinit(SB)
CALL    runtime·hashinit(SB)
CALL    runtime·schedinit(SB)

// create a new goroutine to start program
PUSHQ    $runtime·main·f(SB)        // entry
PUSHQ    $0            // arg size
CALL    runtime·newproc(SB)
POPQ    AX
POPQ    AX

// start this M
CALL    runtime·mstart(SB)复制代码

接下来就进入分析环节

1,通过osinit函数还获取cpu个数和page的大小,这块挺简单的
2,接下来看看schedinit函数(跟本节相关的重要代码)

func schedinit() {
    //获取当前的G
    _g_ := getg()
    if raceenabled {
        _g_.racectx, raceprocctx0 = raceinit()
    }
    //设置M的最大数量
    sched.maxmcount = 10000
    //初始化栈空间
    stackinit()
    //内存空间初始化操作
    mallocinit()
    //初始化当前的M
    mcommoninit(_g_.m)

    //将P的数量调整为CPU数量
    procs := ncpu
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
    }
    if procs > _MaxGomaxprocs {
        procs = _MaxGomaxprocs
    }
    //初始化P
    if procresize(procs) != nil {
        throw("unknown runnable goroutine during bootstrap")
    }

}复制代码

3,上面我们可以看到调用了procresize函数来初始化P,那么我们来看下procresize函数。这块代码过长,分几个部分解析(只贴重要的代码)
(1) 初始化新的P

for i := int32(0); i < nprocs; i++ {
        pp := allp[i]
        if pp == nil {
            //新建一个P对象
            pp = new(p)
            pp.id = i
            pp.status = _Pgcstop
            //保存到allp数组(负责存储P的数组)
            atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
        }
        //如果P还没有cache,那么进行分配
        if pp.mcache == nil {
            if old == 0 && i == 0 {
                if getg().m.mcache == nil {
                    throw("missing mcache?")
                }
                pp.mcache = getg().m.mcache // bootstrap
            } else {
                pp.mcache = allocmcache()//分配cache
            }
        }
    }复制代码

(2) 释放没被使用的P

for i := nprocs; i < old; i++ {
        p := allp[i]
        // 将本地任务添加到全局队列中
        for p.runqhead != p.runqtail {
            p.runqtail--
            gp := p.runq[p.runqtail%uint32(len(p.runq))].ptr()
            // 插入全局队列的头部
            globrunqputhead(gp)
        }
        //释放P所绑定的cache
        freemcache(p.mcache)
        p.mcache = nil
        //将当前的P的G复用链接到全局
        gfpurge(p)
        p.status = _Pdead
        // can't free P itself because it can be referenced by an M in syscall
    }复制代码

经过这两个步骤后,那么我们就创建了一批的P,闲置的P会被放进调度器Sched的空闲链表中

二,创建G的过程

从上面的汇编代码可以看出接下来会去调用newproc函数来创建主G,然后用这个主函数去执行runtime.main,然后创建一个线程(这个线程在运行期间专门负责系统监控),接下来就进入GO程序中的main函数去运行了。
先看下newproc代码

func newproc(siz int32, fn *funcval) {
    argp := add(unsafe.Pointer(&fn), sys.PtrSize)//获取参数的地址
    pc := getcallerpc(unsafe.Pointer(&siz))//获取调用方的PC支
    systemstack(func() {
        newproc1(fn, (*uint8)(argp), siz, 0, pc)//真正创建G的地方
    })
}复制代码

接下来看下newpro1的主要代码

func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {
    //从当前P复用链表来获取G
    _p_ := _g_.m.p.ptr()
    newg := gfget(_p_)
    //如果获取失败,则新建一个
    if newg == nil {
        newg = malg(_StackMin)
        casgstatus(newg, _Gidle, _Gdead)
        allgadd(newg) 
    }
    //将得到的G放入P的运行队列中
    runqput(_p_, newg, true)
    //下面三个条件分别为:是否有空闲的P;M是否处于自旋状态;当前是否创建runteime.main
    if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && runtimeInitTime != 0 {
        wakep()
    }

}复制代码

这个wakep()函数的代码也是值得一看的,这个思想可以用到平时的代码编程中去

func wakep() {
    //线程被唤醒后需要绑定一个P,这里使用cas操作,可以避免唤醒过多线程,这里也对应了上面的三个判断条件之一
    if !atomic.Cas(&sched.nmspinning, 0, 1) {
        return
    }
    startm(nil, true)
}复制代码

startm的代码就留给读者自己去看了,不然感觉整个博文都是代码,主要的思想是:获取一个空闲的P(如果传入的P为空),然后先尝试获取空闲M(空闲的M被调度器schedt管理,这个结构体也可以去看下),获取不到再去创建一个M等。

三,Channel

这块就稍微比较简单了,代码也不多,但是看下来收获还是很多的

1,创建Channel

先看下结构体定义(有删减)

type hchan struct {
    qcount   uint           // 队列中数据个数
    dataqsiz uint           // 缓冲槽大小
    buf      unsafe.Pointer // 指向缓冲槽的指针
    elemsize uint16         // 数据大小
    closed   uint32         // 表示 channel 是否关闭
    elemtype *_type // 数据类型
    sendx    uint   // 发送位置索引
    recvx    uint   // 接收位置索引
    recvq    waitq  // 接收等待列表
    sendq    waitq  // 发送等待列表
    lock mutex      // 锁
}
type sudog struct {
    g          *g
    selectdone *uint32 // CAS to 1 to win select race (may point to stack)
    next       *sudog
    prev       *sudog
    elem       unsafe.Pointer // data element (may point to stack)
    waitlink    *sudog // g.waiting list or semaRoot
    waittail    *sudog // semaRoot
    c           *hchan // channel
}复制代码

上面的recvq其实是读操作阻塞在channel的G列表,sendq其实是写操作阻塞在channel的G列表,那么G可以同时阻塞在不同的channel上,那么如何解决呢?这时候就引入了sudog,它其实是对G的一个包装,代表在等待队列上的一个G。

接下来看看创建过程

func makechan(t *chantype, size int64) *hchan {
    elem := t.elem

    // 大小不超过64K
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
    var c *hchan
    // 整个创建过程还是简单明了的
    if elem.kind&kindNoPointers != 0 || size == 0 {
        //一次性分配内存
        c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true))
        if size > 0 && elem.size != 0 {
            c.buf = add(unsafe.Pointer(c), hchanSize)
        } else {
            c.buf = unsafe.Pointer(c)
        }
    } else {
        c = new(hchan)
        c.buf = newarray(elem, int(size))
    }
    //设置数据大小,类型和缓冲槽大小
    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)

    return c
}复制代码

2,发送

send函数的代码有点长,接下来就拆分进行说明
(1) 如果recvq有G在阻塞,那么就从该队列取出该G,将数据给该G

if sg := c.recvq.dequeue(); sg != nil {
        // Found a waiting receiver. We pass the value we want to send
        // directly to the receiver, bypassing the channel buffer (if any).
        send(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true
    }复制代码

(2) 如果hchan.buf还有可用的空间,那么就将数据放入

//通过比较qcount和datasiz来判断是否还有可用空间
if c.qcount < c.dataqsiz {
        // 将数据放入buf中
        qp := chanbuf(c, c.sendx)
        if raceenabled {
            raceacquire(qp)
            racerelease(qp)
        }
        typedmemmove(c.elemtype, qp, ep)
        c.sendx++
        if c.sendx == c.dataqsiz {
            c.sendx = 0
        }
        c.qcount++
        unlock(&c.lock)
        return true
    }复制代码

(3) hchan.buf满了,那么就会阻塞住了

// Block on the channel. Some receiver will complete our operation for us.
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
    mysg.releasetime = -1
}
//初始化一些参数
mysg.elem = ep         
mysg.waitlink = nil
mysg.g = gp
mysg.selectdone = nil
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 将当前 goroutine加入等待队列
c.sendq.enqueue(mysg)   
goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)复制代码

这里我们就可以看到了,如果满了,那么sudog就会出现了,通过初始化后代表当前G进入等待队列

3,接收

同理,接收也分为三种情况

(1) 当前有发送goroutine阻塞在channel上,buf满了

if sg := c.sendq.dequeue(); sg != nil {
        recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true, true
    }复制代码

(2) buf中有数据

if c.qcount > 0 {
        // 直接从队列中接收
        qp := chanbuf(c, c.recvx)
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        typedmemclr(c.elemtype, qp)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.qcount--
        unlock(&c.lock)
        return true, true
    }复制代码

(3) buf中无数据了,那么则会阻塞住

    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
    // 同样的,由sudog代表G去排队
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.selectdone = nil
    mysg.c = c
    gp.param = nil
    c.recvq.enqueue(mysg)
    goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)复制代码

总结:虽然这块代码逻辑不复杂,但是设计的东西很多,还是用了很多时间,现在对M执行G的逻辑是懂了,但是还不清楚细节,后面会继续研究。总的读下来,首先第一是对并发的机制可以说是很了解了,对以后在编写相关代码肯定很有帮助。第二,学习到了一些编程思想,例如cas操作,如何更好的进行封装和抽象等。

本书作者带你一步一步深入这些方法。你将理解 Go语言为何选定这些并发模型,这些模型又会带来什么问题,以及你如何组合利用这些模型中的原语去解决问题。学习那些让你在独立且自信的编写与实现任何规模并发系统时所需要用到的技巧和工具。 理解Go语言如何解决并发难以编写正确这一根本问题。 学习并发与并行的关键性区别。 深入到Go语言的内存同步原语。 利用这些模式中的原语编写可维护的并发代码。 将模式组合成为一系列的实践,使你能够编写大规模的分布式系统。 学习 goroutine 背后的复杂性,以及Go语言的运行时如何将所有东西连接在一起。 作者简介 · · · · · · Katherine Cox-Buday是一名计算机科学家,目前工作于 Simple online banking。她的业余爱好包括软件工程、创作、Go 语言(igo、baduk、weiquei) 以及音乐,这些都是她长期的追求,并且有着不同层面的贡献。 目录 · · · · · · 前言 1 第1章 并发概述 9 摩尔定律,Web Scale和我们所陷入的混乱 10 为什么并发很难? 12 竞争条件 13 原子性 15 内存访问同步 17 死锁、活锁和饥饿 20 确定并发安全 28 面对复杂性的简单性 31 第2章 对你的代码建模:通信顺序进程 33 并发与并行的区别 33 什么是CSP 37 如何帮助你 40 Go语言并发哲学 43 第3章 Go语言并发组件 47 goroutine 47 sync包 58 WaitGroup 58 互斥锁和读写锁 60 cond 64 once 69 池 71 channel 76 select 语句 92 GOMAXPROCS控制 97 小结 98 第4章 Go语言并发模式 99 约束 99 for-select循环103 防止goroutine泄漏 104 or-channel 109 错误处理112 pipeline 116 构建pipeline的最佳实践 120 一些便利的生成器 126 扇入,扇出 132 or-done-channel 137 tee-channel 139 桥接channel模式 140 队列排队143 context包 151 小结 168 第5章 大规模并发 169 异常传递169 超时和取消 178 心跳 184 复制请求197 速率限制199 治愈异常的goroutine 215 小结 222 第6章 goroutine和Go语言运行时 223 工作窃取223 窃取任务还是续体 231 向开发人员展示所有这些信息 240 尾声 240 附录A 241
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值