다음 칼럼에서는 golang tutorial 칼럼에서 Golang의 select 구현 메커니즘을 자세히 설명하겠습니다. 필요한 친구들에게 도움이 되길 바랍니다!
오늘 셀렉트 플레이를 하다가 문제를 발견했습니다.
클립 1:
func main(){ var count int for { select { case <-time.Tick(time.Millisecond * 500): fmt.Println("咖啡色的羊驼") count++ fmt.Println("count--->" , count) case <-time.Tick(time.Millisecond * 499) : fmt.Println(time.Now().Unix()) count++ fmt.Println("count--->" , count) } } }
클립 2:
func main(){ t1 := time.Tick(time.Second) t2 := time.Tick(time.Second) var count int for { select { case <-t1: fmt.Println("咖啡色的羊驼") count++ fmt.Println("count--->" , count) case <-t2 : fmt.Println(time.Now().Unix()) count++ fmt.Println("count--->" , count) } } }
Two 질문:
1. 위 스니펫의 출력은 무엇입니까?
2. 어떻게 설명할까요?
첫 번째 문제는 실행하기만 하면 확실히 출력 결과가 달라집니다.
Clip 1:
1535673600 count---> 1 1535673600 count---> 2 1535673601 count---> 3
Clip 2:
咖啡色的羊驼 count---> 1 1535673600 count---> 2 咖啡色的羊驼 count---> 3 1535673601 count---> 4
두 번째는 select가 두 번 채널을 모니터링해서 번갈아가며 나타나기 때문에 이해하기 쉽습니다.
그럼 첫 번째에는 왜 하나만 등장하는 걸까요?
이 문제를 해결하기 위해 select의 구현 메커니즘을 수정해야 해서 이 글을 쓰게 되었습니다.
Select에는 주의가 필요한 여러 메커니즘이 있습니다
1.select+case는 고루틴을 차단하고 모니터링하는 데 사용됩니다. 사례가 없으면 select{}만 현재 프로그램에서 goroutine을 모니터링합니다. . , 이때 실제 goroutine이 실행되어야 한다는 점에 유의하세요. 그렇지 않으면 select{}가 패닉을 보고합니다
2. select 아래에 실행 가능한 사례가 여러 개 있는 경우 하나가 무작위로 실행됩니다.
3.Select는 종종 for 루프와 함께 작동하여 채널에서 일어나는 이야기가 있는지 모니터링합니다. 이 시나리오에서는 break는 현재 선택을 종료하기만 하고 종료하지는 않는다는 점에 유의해야 합니다. break TIP/goto를 사용해야 합니다.
4. 버퍼링이 없는 채널이 값을 전달한 후 즉시 닫히면 닫히기 전에 차단됩니다. 채널에 버퍼링이 있는 경우 여러 고루틴이 켜져 있어도 계속해서 후속 값을 받습니다. 동일한 채널을 닫으면 복구 패닉 방법을 사용하여 채널 폐쇄 문제를 확인할 수 있습니다
위 지식 포인트를 읽은 후에도 이 기사의 핵심 의심을 설명할 수 없으므로 계속하세요!
select 메커니즘에 대한 자세한 설명
소스 코드 조각 해석:
func selectgo(sel *hselect) int { // ... // case洗牌 pollslice := slice{unsafe.Pointer(sel.pollorder), int(sel.ncase), int(sel.ncase)} pollorder := *(*[]uint16)(unsafe.Pointer(&pollslice)) for i := 1; i < int(sel.ncase); i++ { //.... } // 给case排序 lockslice := slice{unsafe.Pointer(sel.lockorder), int(sel.ncase), int(sel.ncase)} lockorder := *(*[]uint16)(unsafe.Pointer(&lockslice)) for i := 0; i < int(sel.ncase); i++ { // ... } for i := int(sel.ncase) - 1; i >= 0; i-- { // ... } // 加锁该select中所有的channel sellock(scases, lockorder) // 进入loop loop: // ... // pass 1 - look for something already waiting // 按顺序遍历case来寻找可执行的case for i := 0; i < int(sel.ncase); i++ { //... switch cas.kind { case caseNil: continue case caseRecv: // ... goto xxx case caseSend: // ... goto xxx case caseDefault: dfli = casi dfl = cas } } // 没有找到可以执行的case,但有default条件,这个if里就会直接退出了。 if dfl != nil { // ... } // ... // pass 2 - enqueue on all chans // chan入等待队列 for _, casei := range lockorder { // ... switch cas.kind { case caseRecv: c.recvq.enqueue(sg) case caseSend: c.sendq.enqueue(sg) } } // wait for someone to wake us up // 等待被唤起,同时解锁channel(selparkcommit这里实现的) gp.param = nil gopark(selparkcommit, nil, "select", traceEvGoBlockSelect, 1) // 突然有故事发生,被唤醒,再次该select下全部channel加锁 sellock(scases, lockorder) // pass 3 - dequeue from unsuccessful chans // 本轮最后一次循环操作,获取可执行case,其余全部出队列丢弃 casi = -1 cas = nil sglist = gp.waiting // Clear all elem before unlinking from gp.waiting. for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink { sg1.isSelect = false sg1.elem = nil sg1.c = nil } gp.waiting = nil for _, casei := range lockorder { // ... if sg == sglist { // sg has already been dequeued by the G that woke us up. casi = int(casei) cas = k } else { c = k.c if k.kind == caseSend { c.sendq.dequeueSudoG(sglist) } else { c.recvq.dequeueSudoG(sglist) } } // ... } // 没有的话,再走一次loop if cas == nil { goto loop } // ... bufrecv: // can receive from buffer bufsend: // ... recv: // ... rclose: // ... send: // ... retc: // ... sclose: // send on closed channel }
표시의 편의를 위해 특별히 프로세스를 설명하기 위해 보기 흉한 그림을 만들었습니다.
즉, 선택은 4단계로 수행됩니다.
이 글에서 의심되는 핵심 포인트는 해당 루프에서 실행 파일이 발견되면
이 선택에서 실행되지 않는 경우에 해당하는 채널이 팀의 현재 고루틴에 주어지며,, time.Tick은 조각 2처럼 전역 스택에 있는 대신 그 자리에서 생성되므로 하나가 실행될 때마다 다른 하나는 버려지고 다시 시작되어야 합니다. - 다시 선택하면 새로운 것을 얻으려면 처음부터 다시 시작해야 합니다. 이것은 일시적인 이해입니다. 더 나은 이해가 있으시면 메시지를 남겨주세요. 감사합니다.
위 내용은 Golang의 select 구현 메커니즘의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!