Go select的使用和实现原理
<h2><strong> 一、select简介</strong></h2><p>1.Go的select语句是一种仅能用于channl发送和接收消息的专用语句,此语句运行期间是阻塞的;当select中没有case语句的时候,会阻塞当前groutine。</p>
<p>2.select是Golang在语言层面提供的I/O多路复用的机制,其专门用来检测多个channel是否准备完毕:可读或可写。</p>
<p>3.select语句中除default外,每个case操作一个channel,要么读要么写</p>
<p>4.select语句中除default外,各case执行顺序是随机的</p>
<p>5.select语句中如果没有default语句,则会阻塞等待任一case</p>
<p>6.select语句中读操作要判断是否成功读取,关闭的channel也可以读取</p>
<h2>二、用法例子</h2>
<p> 实例一:select语句中除default外,各case执行顺序是随机的</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">package main
import (
"fmt"
"time"
)
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
go func() {
chan1 <- 1
time.Sleep(5 * time.Second)
}()
go func() {
chan2 <- 1
time.Sleep(5 * time.Second)
}()
select {
case <-chan1:
fmt.Println("chan1 ready.")
case <-chan2:
fmt.Println("chan2 ready.")
default:
fmt.Println("default")
}
fmt.Println("main exit.")
}
</pre>
</div>
<p> </p>
<p>程序中声明两个channel,分别为chan1和chan2,依次启动两个协程,分别向两个channel中写入一个数据就进入睡眠。select语句两个case分别检测chan1和chan2是否可读,如果都不可读则执行default语句。</p>
<p>参考答案:<br>select中各个case执行顺序是随机的,如果某个case中的channel已经ready,则执行相应的语句并退出select流程,如果所有case中的channel都未ready,则执行default中的语句然后退出select流程。另外,由于启动的协程和select语句并不能保证执行顺序,所以也有可能select执行时协程还未向channel中写入数据,所以select直接执行default语句并退出。所以,以下三种输出都有可能:</p>
<p>可能的输出一:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">chan1 ready.
main exit.</pre>
</div>
<p>可能的输出二:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">chan2 ready.
main exit.</pre>
</div>
<p>可能的输出三:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">default
main exit.
</pre>
</div>
<p> </p>
<p>实例二:select语句中如果没有default语句,则会阻塞等待任一case</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">package main
import (
"fmt"
"time"
)
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
writeFlag := false
go func() {
for {
if writeFlag {
chan1 <- 1
}
time.Sleep(time.Second)
}
}()
go func() {
for {
if writeFlag {
chan2 <- 1
}
time.Sleep(time.Second)
}
}()
select {
case <-chan1:
fmt.Println("chan1 ready.")
case <-chan2:
fmt.Println("chan2 ready.")
}
fmt.Println("main exit.")
}
</pre>
</div>
<p> </p>
<p>程序中声明两个channel,分别为chan1和chan2,依次启动两个协程,协程会判断一个bool类型的变量writeFlag来决定是否要向channel中写入数据,由于writeFlag永远为false,所以实际上协程什么也没做。select语句两个case分别检测chan1和chan2是否可读,这个select语句不包含default语句。</p>
<p>参考答案:select会按照随机的顺序检测各case语句中channel是否ready,如果某个case中的channel已经ready则执行相应的case语句然后退出select流程,如果所有的channel都未ready且没有default的话,则会阻塞等待各个channel。所以上述程序会一直阻塞。</p>
<p> 实例三:select语句中读操作要判断是否成功读取,关闭的channel也可以读取</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">package main
import (
"fmt"
)
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
go func() {
close(chan1)
}()
go func() {
close(chan2)
}()
select {
case <-chan1:
fmt.Println("chan1 ready.")
case <-chan2:
fmt.Println("chan2 ready.")
}
fmt.Println("main exit.")
}
</pre>
</div>
<p>实例四:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">package main
func main() {
select {
}
}
</pre>
</div>
<p> </p>
<p>上面程序中只有一个空的select语句。</p>
<p>参考答案:对于空的select语句,程序会被阻塞,准确的说是当前协程被阻塞,同时Golang自带死锁检测机制,当发现当前协程再也没有机会被唤醒时,则会panic。所以上述程序会panic。</p>
<h2>三、select实现原理</h2>
<p>Golang实现select时,定义了一个数据结构表示每个case语句(含defaut,default实际上是一种特殊的case),select执行过程可以类比成一个函数,函数输入case数组,输出选中的case,然后程序流程转到选中的case块。</p>
<h3>case数据结构</h3>
<p>源码包<code>src/runtime/select.go:scase</code>定义了表示case语句的数据结构:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">type scase struct {
c *hchan // chan
kind uint16
elem unsafe.Pointer // data element
}
</pre>
</div>
<p> </p>
<p>scase.c为当前case语句所操作的channel指针,这也说明了一个case语句只能操作一个channel。<br>scase.kind表示该case的类型,分为读channel、写channel和default,三种类型分别由常量定义:</p>
<ul>
<li>caseRecv:case语句中尝试读取scase.c中的数据;</li>
<li>caseSend:case语句中尝试向scase.c中写入数据;</li>
<li>caseDefault: default语句</li>
</ul>
<p>scase.elem表示缓冲区地址,跟据scase.kind不同,有不同的用途:</p>
<ul>
<li>scase.kind == caseRecv : scase.elem表示读出channel的数据存放地址;</li>
<li>scase.kind == caseSend : scase.elem表示将要写入channel的数据存放地址;</li>
</ul>
<h3>select实现逻辑</h3>
<p>源码包<code>src/runtime/select.go:selectgo()</code>定义了select选择case的函数:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)
</pre>
</div>
<p> </p>
<p>函数参数:</p>
<ul>
<li>cas0为scase数组的首地址,selectgo()就是从这些scase中找出一个返回。</li>
<li>order0为一个两倍cas0数组长度的buffer,保存scase随机序列pollorder和scase中channel地址序列lockorder
<ul>
<li>pollorder:每次selectgo执行都会把scase序列打乱,以达到随机检测case的目的。</li>
<li>lockorder:所有case语句中channel序列,以达到去重防止对channel加锁时重复加锁的目的。</li>
</ul>
</li>
<li>ncases表示scase数组的长度</li>
</ul>
<p>函数返回值:</p>
<ol>
<li>int: 选中case的编号,这个case编号跟代码一致</li>
<li>bool: 是否成功从channle中读取了数据,如果选中的case是从channel中读数据,则该返回值表示是否读取成功。</li>
</ol>
<p>selectgo实现伪代码如下:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
//1. 锁定scase语句中所有的channel
//2. 按照随机顺序检测scase中的channel是否ready
// 2.1 如果case可读,则读取channel中数据,解锁所有的channel,然后返回(case index, true)
// 2.2 如果case可写,则将数据写入channel,解锁所有的channel,然后返回(case index, false)
// 2.3 所有case都未ready,则解锁所有的channel,然后返回(default index, false)
//3. 所有case都未ready,且没有default语句
// 3.1 将当前协程加入到所有channel的等待队列
// 3.2 当将协程转入阻塞,等待被唤醒
//4. 唤醒后返回channel对应的case index
// 4.1 如果是读操作,解锁所有的channel,然后返回(case index, true)
// 4.2 如果是写操作,解锁所有的channel,然后返回(case index, false)
}</pre>
</div>
<p> 特别说明:对于读channel的case来说,如<code>case elem, ok := <-chan1:</code>, 如果channel有可能被其他协程关闭的情况下,一定要检测读取是否成功,因为close的channel也有可能返回,此时ok == false。</p>
<p> </p>
<p>参考文献:</p>
<p>https://my.oschina.net/renhc/blog/2253937/nhttps://blog.csdn.net/xdrbt/article/details/80287959</p>
<p>https://blog.csdn.net/wangxindong11/article/details/78591308</p>
<p>https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-select/</p><br><br>
来源:https://www.cnblogs.com/wuyepeng/p/13910678.html
頁:
[1]