逐步学习Go-Select多路复用

内容目录

概述

这里又有多路复用,但是Go中的这个多路复用不同于网络中的多路复用。在Go里,select用于同时等待多个通信操作(即多个channel的发送或接收操作)。Go中的channel可以参考我的文章:逐步学习Go-并发通道chan(channel)

拆字解释

  • 多路:指的是多个channel操作路径。你可以在select块中定义多个case,每个case对应一个channel上的I/O操作(发送或接收)。

  • 复用:指的是select的功能,它可以监听多个channel上的事件,并且仅当其中一个channel准备就绪时才会执行相关操作。这样,单个goroutine可以高效地等待多个并发事件而不是单个事件。

复用的是goroutine,一个goroutine使用select可以监听多个信道。

整体来讲:Select就是为channel设计的。

这张图是参考大佬Dravenss画的

select语法

Go语言中的select关键字功能在概念上与操作系统的select类似,区别在于Go的select是用于goroutine监听多个channel的可读或可写状态。

Go的select允许在channel上进行非阻塞收发,同时当多个channel同时响应时,select会随机执行其中的一个case。

Go的select语句可以包含一个default分支,使得在没有channel准备好时,不会阻塞goroutine,而是执行default分支。


    select {
    case <-ch:
        println("recieved")
    case <-time.After(10 * time.Second):
        println("Timeout")
    default:
        printStr = "Hello Select"
    }

接下来我们来看场景用例。

在下面的场景测试用例中,我们定义了三个channel: ch1, ch2和ch3。

select只有一个case条件满足

我们创建完成三个channel以后,我们只想ch1发送消息,那么在select三个channel时只有 case <- ch1可以满足,用例执行会输出"Recieved ch1"。

file


func TestSelect_ShouldRecvChan1_WhenChan1CaseWasFullfilled(t *testing.T) {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    ch3 := make(chan int, 1)

    chans := []chan int{ch1, ch2, ch3}

    var wg sync.WaitGroup
    wg.Add(1)

    go func() {
        chans[0] <- 1
        wg.Done()
    }()

    wg.Wait()

    select {
    case <-ch1:
        println("Recieved ch1")
    case <-ch2:
        println("Recieved ch2")
    case <-ch3:
        println("Recieved ch3")
    case <-time.After(10 * time.Second):
        println("Timeout")
    default:
    }

}

select有多个case条件满足

在这个场景中,我们使用三个goroutine向三个channel都发送了消息,然后等待所有的goroutine执行完成确保3个channel都接收到了消息,那么select会随机选择一个case条件执行(如果自己测试需要多执行几次,因为不止下一次会执行那个case分支)。

如果select中的多个case同时满足,Go语言如何进行选择。Go语言官方文档规定,当多个case都可以运行时,Go会按照"伪随机"的方式来选择一个case执行。这个"伪随机"是指,它不是完全随机的,而是通过一定的算法进行选择,以防止某个channel在高并发的情况下,出现饿死(被忽略)的情况。
file


func TestSelect_ShouldRandomEnterCaseBranch_WhenAllChannelsCaseWereFullfilled(t *testing.T) {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    ch3 := make(chan int, 1)

    chans := []chan int{ch1, ch2, ch3}

    var wg sync.WaitGroup
    wg.Add(3)

    for i := 0; i < 3; i++ {
        go func(i int) {
            chans[i] <- 1
            wg.Done()
        }(i)
    }

    wg.Wait()

    select {
    case <-ch1:
        println("Recieved ch1")
    case <-ch2:
        println("Recieved ch2")
    case <-ch3:
        println("Recieved ch3")
    case <-time.After(10 * time.Second):
        println("Timeout")
    default:
    }

}

select没有条件满足-阻塞(Deadlock)

在这个场景,我们只是创建了3个channel,但是没有向三个channel发送消息,那么执行select时go会panic, 错误信息为:Deadlock。

file


func TestSelect_ShouldBlock_WhenNoCaseWasFullfilled(t *testing.T) {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    ch3 := make(chan int, 1)

    select {
    case <-ch1:
        println("Recieved ch1")
    case <-ch2:
        println("Recieved ch2")
    case <-ch3:
        println("Recieved ch3")
    }

}

select没有条件满足-超时

在这个场景中,我们创建了三个channel,然后没有向这三个channel中发送消息,最后我们使用select尝试从三个channel中接收消息,但是我们在case中增加了一个超时检测。

在这个场景中,select会在10秒后执行 case <-time.After(10 * time.Second):分支因为管道条件没有被满足且没有default

file


func TestSelect_ShouldTimeout_WhenNoCaseWasFullfilled(t *testing.T) {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    ch3 := make(chan int, 1)

    select {
    case <-ch1:
        println("Recieved ch1")
    case <-ch2:
        println("Recieved ch2")
    case <-ch3:
        println("Recieved ch3")
    case <-time.After(10 * time.Second):
        println("Timeout")
    }

}

select没有条件满足-default

在这个场景中,代码和上面的差别在于我们添加了default分支,添加了default后如果所有case没有条件满足则执行default分支,所以你执行这个用例控制台会打印Default

file


func TestSelect_ShouldRunDefaultBranch_WhenNoCaseWasFullfilledAndHasDefaultBranch(t *testing.T) {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    ch3 := make(chan int, 1)

    select {
    case <-ch1:
        println("Recieved ch1")
    case <-ch2:
        println("Recieved ch2")
    case <-ch3:
        println("Recieved ch3")
    case <-time.After(10 * time.Second):
        println("Timeout")
    default:
        println("Default")
    }

}

select关闭的channel接收

在这个场景中,我们创建3个channel然后理解关闭,最后使用select来读取三个channel,那么根据关闭后chnanel的定义:channel在关闭后永远都可以读取,那么select 的case条件可以被满足且随机选择一个case分支执行,只是读取到的都是“0”值。

注意:0值这个是根据不同类型而不一样的,而且go是严格类型检查,nil是不通用的


func TestSelect_ShouldRecvZeroValue_WhenSelectFromClosedChannel(t *testing.T) {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    ch3 := make(chan int, 1)

    close(ch1)
    close(ch2)
    close(ch3)

    select {
    case value := <-ch1:
        if value == 0 {
            println("Recieved zero value from ch1")
        } else {
            println("Recieved ch1")
        }
    case value := <-ch2:
        if value == 0 {
            println("Recieved zero value from ch2")
        } else {
            println("Recieved ch2")
        }
    case value := <-ch3:
        if value == 0 {
            println("Recieved zero value from ch3")
        } else {
            println("Recieved ch3")
        }
    case <-time.After(10 * time.Second):
        println("Timeout")
    default:
        println("Default")
    }
}

select 在关闭的channel上发送

在这个场景中,我们向通过select的case分支来向关闭的channel发送数据,根据go channel的定义:会发生panic。

如下截图,我们的UT显示PASS,表示发生了Panic,当然你也可以改变一下把assert.Panics注释掉,直接执行select的,那么你会得到第二张图的结果:"send on closed channel"

file

file

func TestSelect_ShouldPanic_WhenSendToClosedChannel(t *testing.T) {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    ch3 := make(chan int, 1)

    close(ch1)
    close(ch2)
    close(ch3)

    assert.Panics(t, func() {
        select {
        case ch1 <- 1:
            println("send ch1")
        case ch2 <- 1:
            println("send ch2")
        case ch3 <- 1:
            println("send ch3")
        case <-time.After(10 * time.Second):
            println("Timeout")
        default:
            println("Default")
        }
    })

}

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部