当前位置: 首页 > news >正文

golang并发安全-select

前面说了golang的channel, 今天我们看看golang select 是怎么实现的。

数据结构

type scase struct {c    *hchan         // chanelem unsafe.Pointer // 数据
}

select 非默认的case 中都是处理channel 的 接受和发送,所有scase 结构体中c是用来存储select 的case中使用的channel

处理流程

select case 场景

编译器在中间代码生成期间会根据 select 中 case 的不同对控制语句进行优化,这一过程都发生在cmd/compile/internal/walk/select.go 中,下面会根据不同的场景进行分析代码。

没有case

代码示例

func main() {select {}
}

如果是空的select语句,程序会被阻塞,golang 带有死锁监测机制:如果当前写成无法被唤醒,则会panic

源码解读

在runtime/select.go中可以看到:如果cases为空直接调用gopark函数以waitReasonSelectNoCases的原因挂起当前的协程,并且无法被唤醒,golang监测到直接panic。

同样我们在walk/select.go的walkSelectCases函数中可以看到,如果case为空直接调用runtime.block函数

只有一个case

代码示例

func main() {ch := make(chan int)go func() {ch <- 1}()select {case data := <-ch:fmt.Println("ch data:", data)}
}

如果有输入直接打印ch data : 1 , 没有的话会被检测出all goroutines are asleep - deadlock!(和没有case的一样)

源码解读

如果一个非default case ,将读写转换成 ch <- 或 <- ch, 正常的channel读写

func walkSelectCases(cases []*ir.CommClause) []ir.Node {// optimization: one-case select: single op.if ncas == 1 {cas := cases[0] //获取caseir.SetPos(cas)l := cas.Init()if cas.Comm != nil { // 不是默认n := cas.Comm // 获取case的条件语句l = append(l, ir.TakeInit(n)...)switch n.Op() {default:base.Fatalf("select %v", n.Op())case ir.OSEND: // 如果是 send, 无须处理// already okcase ir.OSELRECV2:r := n.(*ir.AssignListStmt)// 如果不是 data, ok := <- ch 类型,处理成<- chif ir.IsBlank(r.Lhs[0]) && ir.IsBlank(r.Lhs[1]) {n = r.Rhs[0]break}// 是的话, op设置成data, ok := <- ch形式r.SetOp(ir.OAS2RECV)}l = append(l, n)}// 将case 条件后要执行的语句加入带执行的列表l = append(l, cas.Body...)// 加入 break类型,跳出select-case l = append(l, ir.NewBranchStmt(base.Pos, ir.OBREAK, nil))return l}// convert case value arguments to addresses.// this rewrite is used by both the general code and the next optimization.var dflt *ir.CommClausefor _, cas := range cases {ir.SetPos(cas)n := cas.Commif n == nil {dflt = cascontinue}switch n.Op() {case ir.OSEND:n := n.(*ir.SendStmt)n.Value = typecheck.NodAddr(n.Value)n.Value = typecheck.Expr(n.Value)case ir.OSELRECV2:n := n.(*ir.AssignListStmt)if !ir.IsBlank(n.Lhs[0]) {n.Lhs[0] = typecheck.NodAddr(n.Lhs[0])n.Lhs[0] = typecheck.Expr(n.Lhs[0])}}}
}

两个case(一个default)

代码示例

func main() {ch := make(chan int)select {case data := <-ch:fmt.Println("ch data:", data)default:fmt.Println("default")}
}

如果写入就走<- 读取,反之走默认

源码解读

如果是两个case,其中一个是default,非default的会根据send还是recv 调用channel的selectnbsend和 selectnbrecv。这两个方法是非阻塞的

func walkSelectCases(cases []*ir.CommClause) []ir.Node){// optimization: two-case select but one is default: single non-blocking op.if ncas == 2 && dflt != nil {cas := cases[0]if cas == dflt { // 如果是default 放在 cases[1]cas = cases[1]}n := cas.Commir.SetPos(n)r := ir.NewIfStmt(base.Pos, nil, nil, nil)r.SetInit(cas.Init())var cond ir.Nodeswitch n.Op() {default:base.Fatalf("select %v", n.Op())case ir.OSEND:// 调用selectnbsend(c, v)// if selectnbsend(c, v) { body } else { default body }n := n.(*ir.SendStmt)ch := n.Chancond = mkcall1(chanfn("selectnbsend", 2, ch.Type()), types.Types[types.TBOOL], r.PtrInit(), ch, n.Value)case ir.OSELRECV2:n := n.(*ir.AssignListStmt)recv := n.Rhs[0].(*ir.UnaryExpr)ch := recv.Xelem := n.Lhs[0]if ir.IsBlank(elem) { //空的话 elem= NodNilelem = typecheck.NodNil()}cond = typecheck.Temp(types.Types[types.TBOOL])// 调用 selectnbrecvfn := chanfn("selectnbrecv", 2, ch.Type())call := mkcall1(fn, fn.Type().Results(), r.PtrInit(), elem, ch)as := ir.NewAssignListStmt(r.Pos(), ir.OAS2, []ir.Node{cond, n.Lhs[1]}, []ir.Node{call})r.PtrInit().Append(typecheck.Stmt(as))}r.Cond = typecheck.Expr(cond)r.Body = cas.Bodyr.Else = append(dflt.Init(), dflt.Body...)return []ir.Node{r, ir.NewBranchStmt(base.Pos, ir.OBREAK, nil)}}    
}

每次尝试从channel读/写值,如果不成功则直接返回,不会阻塞。从selectnbsend和selectnbrecv看出,最后转换成if-else

// compiler implements
//
//	select {
//	case c <- v:
//		... foo
//	default:
//		... bar
//	}
//
// as
//
//	if selectnbsend(c, v) {
//		... foo
//	} else {
//		... bar
//	}
//
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {// block:false// chan将 select准换if-elsereturn chansend(c, elem, false, getcallerpc())
}// compiler implements
//
//	select {
//	case v, ok = <-c:
//		... foo
//	default:
//		... bar
//	}
//
// as
//
//	if selected, ok = selectnbrecv(&v, c); selected {
//		... foo
//	} else {
//		... bar
//	}
//
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {// block:false// chan将 select准换if-elsereturn chanrecv(c, elem, false)
}

多个case

代码示例

func main() {ch := make(chan int)go func() {tempArr := []int{1,2,3,4,5,6}for i := range tempArr {ch <- i}}()go func() {for {select {case i := <-ch:println("first: ", i)case i := <-ch:println("second", i)}}}()time.Sleep(3 * time.Second)}

可以看到多个case,会随机选取一个case执行

源码解读

func walkSelectCases(cases []*ir.CommClause) []ir.Node {ncas := len(cases)sellineno := base.Posif dflt != nil {ncas--}// 定义casorder为ncas大小的case语句的数组casorder := make([]*ir.CommClause, ncas)// 分别定义nsends为发送channel的case个数,nrecvs为接收channel的case个数nsends, nrecvs := 0, 0// 多case编译后待执行的语句列表var init []ir.Node// generate sel-structbase.Pos = sellineno// 定义selv为长度为ncas的scase类型的数组// scasetype()函数返回的就是scase结构体,包含c和elem两个字段selv := typecheck.Temp(types.NewArray(scasetype(), int64(ncas)))init = append(init, typecheck.Stmt(ir.NewAssignStmt(base.Pos, selv, nil)))// No initialization for order; runtime.selectgo is responsible for that.// 定义order为2倍的ncas长度的TUINT16类型的数组// 注意:selv和order作为runtime.selectgo()函数的入参,前者存放scase列表内存地址,后者用来做scase排序使用,排序是为了便于挑选出待执行的caseorder := typecheck.Temp(types.NewArray(types.Types[types.TUINT16], 2*int64(ncas)))var pc0, pcs ir.Nodeif base.Flag.Race {pcs = typecheck.Temp(types.NewArray(types.Types[types.TUINTPTR], int64(ncas)))pc0 = typecheck.Expr(typecheck.NodAddr(ir.NewIndexExpr(base.Pos, pcs, ir.NewInt(0))))} else {pc0 = typecheck.NodNil()}// register cases 遍历case生成scase对象放到selv中for _, cas := range cases {ir.SetPos(cas)init = append(init, ir.TakeInit(cas)...)n := cas.Commif n == nil { // default:continue}var i intvar c, elem ir.Nodeswitch n.Op() { // 根据类型获取chan, elem的值default:base.Fatalf("select %v", n.Op())case ir.OSEND: // 发送chan类型,i从0开始递增n := n.(*ir.SendStmt)i = nsendsnsends++c = n.Chanelem = n.Valuecase ir.OSELRECV2: // 接收chan,i从ncas开始递减n := n.(*ir.AssignListStmt)nrecvs++i = ncas - nrecvsrecv := n.Rhs[0].(*ir.UnaryExpr)c = recv.Xelem = n.Lhs[0]}casorder[i] = cas// 定义一个函数,写入c或elem到selv数组setField := func(f string, val ir.Node) {// 放到selv数组r := ir.NewAssignStmt(base.Pos, ir.NewSelectorExpr(base.Pos, ir.ODOT, ir.NewIndexExpr(base.Pos, selv, ir.NewInt(int64(i))), typecheck.Lookup(f)), val)// 添加到带执行列表init = append(init, typecheck.Stmt(r))}c = typecheck.ConvNop(c, types.Types[types.TUNSAFEPTR])setField("c", c)if !ir.IsBlank(elem) {elem = typecheck.ConvNop(elem, types.Types[types.TUNSAFEPTR])setField("elem", elem)}// TODO(mdempsky): There should be a cleaner way to// handle this.if base.Flag.Race {r := mkcallstmt("selectsetpc", typecheck.NodAddr(ir.NewIndexExpr(base.Pos, pcs, ir.NewInt(int64(i)))))init = append(init, r)}}// 如果发送chan和接收chan的个数不等于ncas,直接报错if nsends+nrecvs != ncas {base.Fatalf("walkSelectCases: miscount: %v + %v != %v", nsends, nrecvs, ncas)}// run the select  开始执行select动作base.Pos = sellineno// 定义chosen, recvOK作为selectgo()函数的两个返回值// chosen 表示被选中的case的索引,recvOK表示对于接收操作,是否成功接收chosen := typecheck.Temp(types.Types[types.TINT])recvOK := typecheck.Temp(types.Types[types.TBOOL])r := ir.NewAssignListStmt(base.Pos, ir.OAS2, nil, nil)r.Lhs = []ir.Node{chosen, recvOK}// 调用runtime.selectgo()函数作为运行时实际执行多case的select动作的函数fn := typecheck.LookupRuntime("selectgo")var fnInit ir.Nodesr.Rhs = []ir.Node{mkcall1(fn, fn.Type().Results(), &fnInit, bytePtrToIndex(selv, 0), bytePtrToIndex(order, 0), pc0, ir.NewInt(int64(nsends)), ir.NewInt(int64(nrecvs)), ir.NewBool(dflt == nil))}init = append(init, fnInit...)init = append(init, typecheck.Stmt(r))// selv and order are no longer alive after selectgo.// 执行完selectgo()函数后,销毁selv和order数组.init = append(init, ir.NewUnaryExpr(base.Pos, ir.OVARKILL, selv))init = append(init, ir.NewUnaryExpr(base.Pos, ir.OVARKILL, order))if base.Flag.Race {init = append(init, ir.NewUnaryExpr(base.Pos, ir.OVARKILL, pcs))}// dispatch cases //定义一个函数,根据chosen确定的case分支生成if语句,执行该分支的语句dispatch := func(cond ir.Node, cas *ir.CommClause) {cond = typecheck.Expr(cond)cond = typecheck.DefaultLit(cond, nil)r := ir.NewIfStmt(base.Pos, cond, nil, nil)if n := cas.Comm; n != nil && n.Op() == ir.OSELRECV2 {n := n.(*ir.AssignListStmt)if !ir.IsBlank(n.Lhs[1]) {x := ir.NewAssignStmt(base.Pos, n.Lhs[1], recvOK)r.Body.Append(typecheck.Stmt(x))}}r.Body.Append(cas.Body.Take()...)r.Body.Append(ir.NewBranchStmt(base.Pos, ir.OBREAK, nil))init = append(init, r)}// 如果多case中有default分支,并且chosen小于0,执行该default分支if dflt != nil {ir.SetPos(dflt)dispatch(ir.NewBinaryExpr(base.Pos, ir.OLT, chosen, ir.NewInt(0)), dflt)}// 如果有chosen选中的case分支,即chosen等于i,则执行该分支for i, cas := range casorder {ir.SetPos(cas)dispatch(ir.NewBinaryExpr(base.Pos, ir.OEQ, chosen, ir.NewInt(int64(i))), cas)}return init
}

从上面代码可以看出:

1- 初始化过程: 生成scase数组,定义selv 存放scase数组内存地址,定义order 来给scase排序

2- 遍历所有的case ,将case放到带执行列表(不包括default)

3- 调用runtime。selectgo并将selv和order作为入参传入selectgo

4- 根据selectgo返回的chosen来生成if语句,执行对应的case

解锁加锁

加锁的顺序和解锁的顺序相反。


func sellock(scases []scase, lockorder []uint16) {var c *hchanfor _, o := range lockorder {c0 := scases[o].cif c0 != c {c = c0lock(&c.lock)}}
}func selunlock(scases []scase, lockorder []uint16) {// 我们必须非常小心,在解锁最后一把锁后不要触摸sel,因为sel可以在最后一次解锁后立即释放。//考虑以下情况。第一个M调用runtime·park()在runtime·selectgo()中传递sel。//一旦runtime·park()解锁了最后一个锁,另一个M会使调用select的G再次可运行,//并安排其执行。当G在另一个M上运行时,它锁定所有锁并释放sel。现在,如果第一个M触摸sel,它将访问释放的内存。for i := len(lockorder) - 1; i >= 0; i-- {c := scases[lockorder[i]].cif i > 0 && c == scases[lockorder[i-1]].c {continue // will unlock it on the next iteration}unlock(&c.lock)}
}

selectgo

selectgo 处理逻辑

// cas0指向[ncases]scase类型的数组,order0指向[2*ncases]uint16类型的数组(其中ncases必须<=65536)。
// 返回值有两个, chosen 和 recvOK,分别表示选中的case的序号,和对接收操作是否接收成功的布尔值
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {if debugSelect {print("select: cas0=", cas0, "\n")}//==== 执行必要的初始化操作,并生成处理case的两种顺序:轮询顺序polIorder和加锁顺序lockorder。// 为了将scase分配到栈上,这里直接给cas1分配了64KB大小的数组,同理, 给order1分配了128KB大小的数组// NOTE: In order to maintain a lean stack size, the number of scases// is capped at 65536.cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))// ncases个数 = 发送chan个数+ 接收chan个数ncases := nsends + nrecvs// scases是cas1数组的前ncases个元素scases := cas1[:ncases:ncases]// 顺序列表pollorder是order1的0- ncases个元素pollorder := order1[:ncases:ncases]// 加锁列表lockorder是order1的ncase到 2 ncases 个元素lockorder := order1[ncases:][:ncases:ncases]// NOTE: 编译器初始化的pollorder/lockorder的基础数组不是零。// Even when raceenabled is true, there might be select// statements in packages compiled without -race (e.g.,// ensureSigM in runtime/signal_unix.go).var pcs []uintptrif raceenabled && pc0 != nil {pc1 := (*[1 << 16]uintptr)(unsafe.Pointer(pc0))pcs = pc1[:ncases:ncases]}casePC := func(casi int) uintptr {if pcs == nil {return 0}return pcs[casi]}var t0 int64if blockprofilerate > 0 {t0 = cputicks()}// 生成排列顺序norder := 0for i := range scases {cas := &scases[i]// Omit cases without channels from the poll and lock orders.// 处理case中channel为空的情况if cas.c == nil {cas.elem = nil // 便于GCcontinue}// 通过fastrandn函数引入随机性,确定pollorder列表中case的随机顺序索引j := fastrandn(uint32(norder + 1))pollorder[norder] = pollorder[j]pollorder[j] = uint16(i)norder++}// 重新生成列表pollorder = pollorder[:norder]lockorder = lockorder[:norder]// 根据chan地址确定lockorder加锁排序列表的顺序// 简单的堆排序,以保证nlogn时间复杂度完成排序for i := range lockorder {j := i// 从轮询顺序开始,在同一channel上排序。c := scases[pollorder[i]].cfor j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {k := (j - 1) / 2lockorder[j] = lockorder[k]j = k}lockorder[j] = pollorder[i]}for i := len(lockorder) - 1; i >= 0; i-- {o := lockorder[i]c := scases[o].clockorder[i] = lockorder[0]j := 0for {k := j*2 + 1if k >= i {break}if k+1 < i && scases[lockorder[k]].c.sortkey() < scases[lockorder[k+1]].c.sortkey() {k++}if c.sortkey() < scases[lockorder[k]].c.sortkey() {lockorder[j] = lockorder[k]j = kcontinue}break}lockorder[j] = o}if debugSelect {for i := 0; i+1 < len(lockorder); i++ {if scases[lockorder[i]].c.sortkey() > scases[lockorder[i+1]].c.sortkey() {print("i=", i, " x=", lockorder[i], " y=", lockorder[i+1], "\n")throw("select: broken sort")}}}// 锁定select中涉及的所有channelsellock(scases, lockorder)var (gp     *gsg     *sudogc      *hchank      *scasesglist *sudogsgnext *sudogqp     unsafe.Pointernextp  **sudog)// === pass 1 - 查找可以等待处理的channelvar casi intvar cas *scasevar caseSuccess boolvar caseReleaseTime int64 = -1var recvOK boolfor _, casei := range pollorder {casi = int(casei) // case的索引cas = &scases[casi]  c = cas.cif casi >= nsends { // 处理接收channel的casesg = c.sendq.dequeue()if sg != nil {// 如果当前channel的sendq上有等待的goroutine,// 跳到recv代码 并从缓冲区读取数据后将等待goroutine中的数据放入到缓冲区中相同的位置goto recv}if c.qcount > 0 {//如果当前channel的缓冲区不为空,就会跳到bufrecv标签处从缓冲区获取数据;goto bufrecv}if c.closed != 0 {//如果当前channel已经被关闭,就会跳到rclose读取末尾数据和收尾工作;goto rclose}} else { // 处理发送channel的caseif raceenabled {racereadpc(c.raceaddr(), casePC(casi), chansendpc)}if c.closed != 0 {// 如果当前channel已经被关闭就会直接跳到sclose标签(panic中止程序)goto sclose}sg = c.recvq.dequeue()if sg != nil {// 如果当前channel的recvq上有等待的goroutine,就会跳到 send标签向channel发送数据;goto send}if c.qcount < c.dataqsiz {// 如果当前channel的缓冲区存在空闲位置,就会将待发送的数据存入缓冲区;goto bufsend}}}if !block { // 如果是非阻塞,即包含default分支,解锁所有channel并返回selunlock(scases, lockorder)casi = -1goto retc}// === pass 2 - 将当前goroutine根据需要挂在chan的sendq或recvq上gp = getg() // 获取当前的groutineif gp.waiting != nil {throw("gp.waiting != nil")}nextp = &gp.waiting // 正在等待的sudog结构;按锁定顺序for _, casei := range lockorder {casi = int(casei)cas = &scases[casi]c = cas.csg := acquireSudog()// 获取sudog,将当前goroutine绑定到sudog上sg.g = gpsg.isSelect = true// 在分配elem和在gp.waiting上排队sg之间没有堆栈分割,copystack可以找到它。sg.elem = cas.elemsg.releasetime = 0if t0 != 0 {sg.releasetime = -1}sg.c = c// 按锁定顺序构建waiting list 。*nextp = sgnextp = &sg.waitlink// 加入相应等待队列if casi < nsends {c.sendq.enqueue(sg)} else {c.recvq.enqueue(sg)}}// 被唤醒后会根据 param 来判断是否是由 close 操作唤醒的,所以先置为 nilgp.param = nil// Signal to anyone trying to shrink our stack that we're about// to park on a channel. The window between when this G's status// changes and when we set gp.activeStackChans is not safe for// stack shrinking.atomic.Store8(&gp.parkingOnChan, 1)// 挂起当前goroutinegopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)gp.activeStackChans = false// 加锁所有的channelsellock(scases, lockorder)gp.selectDone = 0sg = (*sudog)(gp.param)// param 存放唤醒 goroutine 的 sudog,如果是关闭操作唤醒的,那么就为 nilgp.param = nil// === pass 3 - 当前 Goroutine 被唤醒之后找到满足条件的 Channel 并进行处理//dequeue from unsuccessful chans// otherwise they stack up on quiet channels// record the successful case, if any.// We singly-linked up the SudoGs in lock order.// 从不成功的通道中退出队列,否则它们会堆积在安静的通道上,记录成功的案例(如果有的话)。我们单独将SudoG按锁定顺序连接起来。casi = -1cas = nilcaseSuccess = false// 当前goroutine 的 waiting 链表按照lockorder顺序存放着case的sudogsglist = gp.waiting// 在从 gp.waiting 取消case的sudog链接之前清除所有元素,便于GCfor sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {sg1.isSelect = falsesg1.elem = nilsg1.c = nil}// 清楚当前goroutine的waiting链表,因为被sg代表的协程唤醒了gp.waiting = nilfor _, casei := range lockorder {k = &scases[casei]// 如果相等说明,goroutine是被当前case的channel收发操作唤醒的if sg == sglist {// sg唤醒了当前goroutine, 则当前G已经从sg的队列中出队,这里不需要再次出队casi = int(casei)cas = kcaseSuccess = sglist.successif sglist.releasetime > 0 {caseReleaseTime = sglist.releasetime}} else {// 不是此case唤醒当前goroutine, 将goroutine从case对应的队列(发送或接收)出队c = k.cif int(casei) < nsends {c.sendq.dequeueSudoG(sglist)} else {c.recvq.dequeueSudoG(sglist)}}// 释放当前case的sudog,然后处理下一个case的sudogsgnext = sglist.waitlinksglist.waitlink = nilreleaseSudog(sglist)sglist = sgnext}if cas == nil {throw("selectgo: bad wakeup")}c = cas.cif debugSelect {print("wait-return: cas0=", cas0, " c=", c, " cas=", cas, " send=", casi < nsends, "\n")}if casi < nsends {if !caseSuccess {goto sclose}} else {recvOK = caseSuccess}if raceenabled {if casi < nsends {raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)} else if cas.elem != nil {raceWriteObjectPC(c.elemtype, cas.elem, casePC(casi), chanrecvpc)}}if msanenabled {if casi < nsends {msanread(cas.elem, c.elemtype.size)} else if cas.elem != nil {msanwrite(cas.elem, c.elemtype.size)}}if asanenabled {if casi < nsends {asanread(cas.elem, c.elemtype.size)} else if cas.elem != nil {asanwrite(cas.elem, c.elemtype.size)}}selunlock(scases, lockorder)goto retcbufrecv:// 能从buffer获取数据if raceenabled {if cas.elem != nil {raceWriteObjectPC(c.elemtype, cas.elem, casePC(casi), chanrecvpc)}racenotify(c, c.recvx, nil)}if msanenabled && cas.elem != nil {msanwrite(cas.elem, c.elemtype.size)}if asanenabled && cas.elem != nil {asanwrite(cas.elem, c.elemtype.size)}recvOK = trueqp = chanbuf(c, c.recvx)if cas.elem != nil {typedmemmove(c.elemtype, cas.elem, qp)}typedmemclr(c.elemtype, qp)c.recvx++if c.recvx == c.dataqsiz {c.recvx = 0}c.qcount--selunlock(scases, lockorder)goto retcbufsend:// 发送数据到缓存if raceenabled {racenotify(c, c.sendx, nil)raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)}if msanenabled {msanread(cas.elem, c.elemtype.size)}if asanenabled {asanread(cas.elem, c.elemtype.size)}typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem)c.sendx++if c.sendx == c.dataqsiz {c.sendx = 0}c.qcount++selunlock(scases, lockorder)goto retcrecv:// 从休眠sender(sg)接收recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)if debugSelect {print("syncrecv: cas0=", cas0, " c=", c, "\n")}recvOK = truegoto retcrclose:// 读取结束的channelselunlock(scases, lockorder)recvOK = falseif cas.elem != nil {typedmemclr(c.elemtype, cas.elem)}if raceenabled {raceacquire(c.raceaddr())}goto retcsend:// 想休眠的接收房发送数据if raceenabled {raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)}if msanenabled {msanread(cas.elem, c.elemtype.size)}if asanenabled {asanread(cas.elem, c.elemtype.size)}send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)if debugSelect {print("syncsend: cas0=", cas0, " c=", c, "\n")}goto retcretc:if caseReleaseTime > 0 {blockevent(caseReleaseTime-t0, 1)}return casi, recvOKsclose:// 向关闭的channel发送数据selunlock(scases, lockorder)panic(plainError("send on closed channel"))
}

总结

简单总结下select对case处理逻辑:

1- 空的case 会被golang监听到无法唤醒的协程,会panic

2- 如果只有一个case, 根据操作类型转换成 <- ch 或 成ch <- () (会跳用channel 的 chansend , chanrecv)

3- 如果一个default 一个非default 的case,非default会走 selectnbsend 和 selectnbrecv 非阻塞的方法(最后转换成if-else 语句)

4- 多个case 的情况下, cmd/compile/internal/walk/select.go 优化程序中:

4.1 对 scase 数组, selv ,order数组初始化,将case放在带执行列表中

4.2 调用selectgo函数,根据返回的chosen 结果来生成if语句,执行对应的case

selectgo 函数:

1- 随机生成一个便利case 的 轮询 poollorder, 根据channel 地址生成一个枷锁顺序的lockorder。(随机顺序保证公平性,加锁顺序能够避免思索)

2- 根据pollorder顺序查找cases是否包含立即处理的chan, 如果有就处理。没有处理的话,创建 sudo 结构,将当前的G 加入各case的channel 对应的 接收发送队列,等待其他G唤醒

3- 当调度器 唤醒当前的G,会按照lockorder ,访问所有的case。从中找到需要处理的case进行读写处理,同时从所有的case 的发送姐搜队列中移除当前的

相关文章:

golang并发安全-select

前面说了golang的channel&#xff0c; 今天我们看看golang select 是怎么实现的。 数据结构 type scase struct {c *hchan // chanelem unsafe.Pointer // 数据 } select 非默认的case 中都是处理channel 的 接受和发送&#xff0c;所有scase 结构体中c是用来存储…...

微软Visual Studio产品之Visual C++编程进阶——一维数组(画画版)

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;看到不少初学者在学习编程语言的过程中如此的痛苦&#xff0c;我决定做点什么&#xff0c;我小时候喜欢看小人书&#xff08;连环画&#xff09;&#xff0c;在那个没有电视、没有手机的年代&#xff0c;这是…...

Moonbeam生态项目分析 — — 下一代DeFi协议HydraDX

作者&#xff1a;David 概览 HydraDX是一个基于Substrate区块链框架构建的DeFi协议&#xff0c;旨在为波卡带来大量流动性。HydraDX的特色服务&#xff0c;HydraDX Omnipool&#xff0c;是一种创新的自动化做市商&#xff08;AMM&#xff09;&#xff0c;通过将所有资产组合在…...

Spark九:Spark调优之Shuffle调优

Spark shuffle调优方法 map端和reduce端缓存大小设置&#xff0c;reduce端重试次数和等待时间间隔&#xff0c;以及bypass设置 学习资料&#xff1a;https://mp.weixin.qq.com/s/caCk3mM5iXy0FaXCLkDwYQ 一、map和reduce端缓冲区大小 1.1 map端 在Spark任务运行过程中&…...

linux c多线程优先级

在 Linux 系统中&#xff0c;可以使用 pthread_setschedparam 函数来设置线程的优先级。该函数需要传入一个指向 pthread_t 类型的线程 ID&#xff0c;以及一个指向 struct sched_param 类型的结构体对象。struct sched_param 结构体包含了线程的优先级信息。 下面是一个示例代…...

Redis在项目开发中的应用

Spring Boot集成Redis构建博客应用 在这个示例中&#xff0c;我们将展示如何使用Spring Boot和Redis构建一个简单的博客应用&#xff0c;包括文章发布、点赞和评论功能。 1. 添加依赖 首先&#xff0c;我们需要在pom.xml文件中添加Spring Boot和Redis的依赖项。 <!-- Sp…...

mapper向mapper.xml传参中文时的乱码问题

1.起因&#xff1a; 在idea中进行模糊查询传参时&#xff0c;发现在idea中查中文查不出记录&#xff0c;在navicate中可以查出来。 2.猜测&#xff1a; 1.idea中的编码问题导致的乱码。 2.idea和navicate的编码一致性导致的乱码。 3.mapper向mapper.xml传参后出现乱码。 3.解…...

基于Docker官方php:7.1.33-fpm镜像构建支持67个常见模组的php7.1.33镜像

实践说明&#xff1a;基于RHEL7(CentOS7.9)部署docker环境(23.0.1、24.0.2)&#xff0c;所构建的php7.1.33镜像应用于RHEL7-9(如AlmaLinux9.1)&#xff0c;但因为docker的特性&#xff0c;适用场景是不限于此的。 文档形成时期&#xff1a;2017-2023年 因系统或软件版本不同&am…...

Type-C PD充电器受电端sink诱骗取电汇总:小家电应用5V9V12V15V20V28V

小家电产品、美容产品、电动产品等升级采用Type-C接口&#xff0c;在Type-C接口上使用Type-C取电芯片&#xff0c;即可使用快速充电器的5V、9V、12V、15V、20V供电&#xff0c;无需再配充电器&#xff0c;各类品牌的充电器都可以支持。目前充电器常见的USB-PD功率为&#xff1a…...

禁用code server docker容器中的工作区信任提示

VSCode 添加受限模式&#xff0c;主要是防止自动运行代码的&#xff0c;比如在vscode配置的task和launch参数是可以运行自定义代码的。如果用VScode打开未知的工程文件就有可能直接运行恶意代码。 但是当我们的实验基础模板文件可控的情况下&#xff0c;要想禁用code server do…...

JSON格式插件-VUE

JsonEditor 安装&#xff1a; npm i bin-code-editor -S引入&#xff1a; import Vue from vue; import CodeEditor from bin-code-editor; import bin-code-editor/lib/style/index.css; import App from ./App.vue; Vue.use(CodeEditor); new Vue({el: #app,render: h > …...

dubbo的springboot集成

1.什么是dubbo&#xff1f; Apache Dubbo 是一款 RPC 服务开发框架&#xff0c;用于解决微服务架构下的服务治理与通信问题&#xff0c;官方提供了 Java、Golang 等多语言 SDK 实现。使用 Dubbo 开发的微服务原生具备相互之间的远程地址发现与通信能力&#xff0c; 利用 Dubbo …...

【人工智能】智能电网:未来能源的革命

未来能源的革命 智能电网革命的意义在于将电力行业从传统的集中式发电和集中式输配电模式转变为智能化、分布式、互动式的能源网络。 现在我们从以下方面详细认真的了解一下智能电网&#xff1a; 智能变电站&#xff0c;智能配电网&#xff0c;智能电能表&#xff0c;智能交互…...

【AIGC】一组精美动物AI智能画法秘诀

如何使用AI绘画&#xff0c;从以下角度&#xff0c;依据表格内容梳理&#xff0c;表格如下&#xff1a; 外貌特征物种姿势特征描述场景风格技术描述小巧可爱幼小浣熊倚在桌子上具有人形特征中世纪酒馆电影风格照明8k分辨率细节精致毛茸茸手持咖啡杯Jean-Baptiste Monge的风格蓝…...

JS 高频面试题

JS 的数据类型有哪些&#xff0c;有什么区别 基本数据类型&#xff08;Undefined、Null、Boolean、Number、String、Symbol&#xff09; 引用数据类型&#xff08;对象、数组和函数&#xff09; 区别&#xff1a; 原始数据类型直接存储在栈&#xff08;stack&#xff09;中的简…...

linux—多服务免密登录

文档结构 概念简介配置操作 概念简介 配置操作 场景&#xff1a;在部署gp集群时&#xff0c;希望 master 节点可以使用gpadmin用户可以实现免密登录 slave1和 slave2 节点&#xff1b; step_1: IP映射 xx.xx.xx.101 server-slave1 xx.xx.xx.102 server-slave2说明&#x…...

【MySQL】数据库之MHA高可用

目录 一、MHA 1、什么是MHA 2、MHA 的组成 3、MHA的特点 4、MHA的工作原理 二、有哪些数据库集群高可用方案 三、实操&#xff1a;一主两从部署MHA 1、完成主从复制 步骤一&#xff1a;完成所有MySQL的配置文件修改 步骤二&#xff1a;完成所有MySQL的主从授权&#x…...

ffmpeg 改变帧率,分辨率,时长等命令

ffmpeg -i elva.mp4 -ss 00:00:20 -t 00:00:30 -c:v copy -c:a copy output1.mp4 视频截取&#xff0c;开始时间和时长,-ss 00:00:20 -t 00:00:30 ffmpeg -i output1.mp4 -c:v libx265 output265.mp4 -c:v libx265,264转265 ffmpeg -i output1.mp4 -c:v libx264 output264.mp4 …...

烟火检测AI边缘计算智能分析网关V4在安防项目中的应用及特点

一、行业背景 随着社会和经济的发展&#xff0c;公共安全和私人安全的需求都在不断增长。人们需要更高效、更准确的安防手段来保障生命财产安全&#xff0c;而人工智能技术正好可以提供这种可能性&#xff0c;通过智能监控、人脸识别、行为分析等手段&#xff0c;大大提高了安防…...

有效的回文

常用方法就是双指针。使用两个指针从字符串的两端向中间移动&#xff0c;同时比较对应位置的字符&#xff0c;直到两个指针相遇。由于题目忽略非字母和非数字的字符且忽略大小写&#xff0c;所以跳过那些字符&#xff0c;并将字母转换为小写&#xff08;或大写&#xff09;进行…...

Electron快速上手

Electron 目录 简介 打包简单的html/css/javascript项目 打包Vue2项目 打包Vue3项目 简介 Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows…...

华为“纯血”鸿蒙加速进场 高校、企业瞄准生态开发新风口

近日&#xff0c;华为终端BG CEO、智能汽车解决方案BU董事长余承东在2024年新年信中提出&#xff0c;开启华为终端未来大发展的新十年。 他特别提到&#xff0c;未来要构建强大的鸿蒙生态&#xff0c;2024年是原生鸿蒙的关键一年&#xff0c;将加快推进各类鸿蒙原生应用的开发…...

抖音百科怎么创建?头条百科的规则和技巧

在玩抖音的时候&#xff0c;不知道注意到抖音的搜索结果没有&#xff0c;有时候会去搜索框搜索一个品牌或人物名称&#xff0c;搜索框下面翻几下大概率就会出现百科词条&#xff0c;这个词条就是抖音百科。抖音的百科属于头条百科&#xff0c;因为这两个平台都属于字节跳动旗下…...

leetcode10-困于环中的机器人

题目链接&#xff1a; https://leetcode.cn/problems/robot-bounded-in-circle/description/?envTypestudy-plan-v2&envIdprogramming-skills 思路&#xff1a; 首先&#xff0c;题目要寻找的是成环的情况。 1.如果经历一次指令后的方向仍为北方&#xff0c;要使得机器人循…...

Linux-shell简单学习

我是南城余&#xff01;阿里云开发者平台专家博士证书获得者&#xff01; 欢迎关注我的博客&#xff01;一同成长&#xff01; 一名从事运维开发的worker&#xff0c;记录分享学习。 专注于AI&#xff0c;运维开发&#xff0c;windows Linux 系统领域的分享&#xff01; 其他…...

CMake入门教程【高级篇】qmake转cmake

&#x1f608;「CSDN主页」&#xff1a;传送门 &#x1f608;「Bilibil首页」&#xff1a;传送门 &#x1f608;「动动你的小手」&#xff1a;点赞&#x1f44d;收藏⭐️评论&#x1f4dd; 文章目录 1. 概述2.qmake与cmake的差异3. qmake示例4.qmake转cmake示例5.MOC、UIC和RCC…...

c#图片作为鼠标光标

图片转换为鼠标光标代码如下&#xff1a; private void Form1_Load(object sender, EventArgs e) {//button1.Cursor System.Windows.Forms.Cursors.Hand;Bitmap bmp new Bitmap("780.jpg");Cursor cursor new Cursor(bmp.GetHicon());button1.Cursor cursor;} …...

微信小程序swiper实现层叠轮播图

在微信小程序中,需要实现展示5个&#xff0c;横向层叠的轮播图效果&#xff0c;轮播图由中间到2侧的依次缩小.如下图 使用原生小程序进行开发,没有使用Skyline模式&#xff0c;所以layout-type配置项也无效。所以基于swiper组件进行调整。 主要思路就是设置不同的样式&#xff…...

揭露欧拉骗局第二篇:逼近公式“Σ1/n=lnn+C”。

Σ1/nlnnC是欧拉为调和级数创造(注意是创造、而不是发现)的“逼近公式”&#xff0c;它在欧系大名鼎鼎&#xff0c;因为它解决了欧洲人百筹莫展的“调和级数求和问题”。 “lnnC”是欧拉的发明&#xff0c;欧拉认为n→∞时&#xff0c;Σ1/nlnn常数&#xff0c;这个常数就是欧…...

MYSQL的学习——单行函数详解

目录 1. 数值函数 1) 基本函数 2) 角度与弧度互换函数 3) 三角函数 4) 指数与对数函数 5) 进制间的转换 2. 字符串函数 3. 日期和时间函数 1) 获取日期、时间 2) 日期与时间戳的转换 3) 获取月份、星期、星期数、天数等函数 4) 日期的操作函数 5) 时间和秒钟转换的…...