在go中监听多个channel

select 关键字

我们可以使用select关键字来同时监听多个goroutine。

复制代码
package main

import (
    "fmt"
    "time"
)

func main() {
    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        c1 <- time.Now().String()
    }()

    go func() {
        time.Sleep(2 * time.Second)
        c2 <- time.Now().String()
    }()

    for i := 0; i < 2; i++ {
        select {
        case res1 := <-c1:
            fmt.Println("from c1:", res1)
        case res2 := <-c2:
            fmt.Println("from c2:", res2)
        }
    }
}

from c1: 2022-09-04 14:30:39.4469184 -0400 EDT m=+1.000172801
from c2: 2022-09-04 14:30:40.4472699 -0400 EDT m=+2.000524401

上面的代码显示了select关键字是如何工作的:

  • 我们首先创建两个 channel c1和c2来监听。
  • 然后我们生成两个goroutine,分别向c1和c2发送当前时间。
  • 在for循环中,我们创建了一个select语句,并定义了两个 case:第一个 case 是我们何时可以从c1接收,第二个 case 是我们何时可以从c2接收。

你可以看到select语句和switch语句在设计上非常相似。两者都定义了不同的情况,并在满足某种情况时运行相应的代码。另外,我们可以看到select语句是阻塞的。也就是说,它将等待,直到其中一个 case 被满足。

我们为这个循环迭代了两次,因为只有两个goroutine需要监听。更确切地说,每个goroutine都是一个fire-and-forget goroutine,意味着它们在返回之前只向一个通道发送一次。因此,在这段代码中,任何时候都有两个消息的最大值,而我们只需要选择两次。

如果我们不知道工作何时会结束呢?

有时我们不知道有多少个工作。在这种情况下,把 select 语句放在一个while循环里。

复制代码
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    c1 := make(chan string)
    rand.Seed(time.Now().UnixNano())

    for i := 0; i < rand.Intn(10); i++ {
        go func() {
            time.Sleep(1 * time.Second)
            c1 <- time.Now().String()
        }()
    }

    for {
        select {
        case res1 := <-c1:
            fmt.Println("from c1:", res1)
        }
    }
}

因为我们让一个随机数的goroutines运行,所以我们不知道有多少个作业。值得庆幸的是,底部包裹着select语句的for循环将捕获每一个输出。让我们看看如果我们运行这段代码会发生什么。

复制代码
from c1: 2022-09-04 14:48:47.5145341 -0400 EDT m=+1.000257801
from c1: 2022-09-04 14:48:47.5146126 -0400 EDT m=+1.000336201
from c1: 2022-09-04 14:48:47.5146364 -0400 EDT m=+1.000359901
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /home/jacob/blog/testing/listening-to-multiple-channels-in-go/main.go:22 +0x128
exit status 2

嗯,select 语句按预期收到了三次,但由于死锁,程序出错了。为什么会出现这种情况?

记住,在没有任何条件的情况下,Go中的for循环会永远运行。这意味着 select 语句将永远尝试接收。然而,要运行的作业数量是有限的。即使没有更多的工作,select 语句仍然会尝试接收。

还记得在本系列的第一篇文章中我说过,如果你在发送方没有准备好的时候试图从一个没有缓冲的通道中接收,你的程序就会陷入死锁。这正是我们例子中的情况。

那么我们如何解决这个问题呢?我们可以使用以前文章中涉及的概念的组合:退出通道和WaitGroups。

复制代码
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func main() {
    c1 := make(chan string)
    exit := make(chan struct{})
    rand.Seed(time.Now().UnixNano())
    var wg sync.WaitGroup

    go func() {
        numJob := rand.Intn(10)
        fmt.Println("number of jobs:", numJob)
        for i := 0; i < numJob; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                time.Sleep(1 * time.Second)
                c1 <- time.Now().String()
            }()
        }
        wg.Wait()
        close(exit)
    }()

    for {
        select {
        case res1 := <-c1:
            fmt.Println("from c1:", res1)
        case <-exit:
            return
        }
    }
}

3
from c1: 2022-09-04 15:09:08.6936976 -0400 EDT m=+1.000287801
from c1: 2022-09-04 15:09:08.6937788 -0400 EDT m=+1.000369101
from c1: 2022-09-04 15:09:08.6937949 -0400 EDT m=+1.000385101
  • 我们创建一个 exit channel 和一个WaitGroup。
  • 每次运行的作业数都是随机的。对于numJobs的次数,我们启动goroutines。为了等待作业的完成,我们把它们加到wg中。当一个工作完成后,我们从wg中减去一个。
  • 一旦所有工作完成,我们就关闭exit channel。
  • 我们将上述部分包裹在一个goroutine中,因为我们希望所有的部分都是无阻塞的。如果我们不把它包在一个goroutine中,wg.Wait()就会等待,直到作业完成。这将阻塞代码,并且不会让底部的for-select语句运行。
  • 此外,由于c1是一个未缓冲的通道,等待所有的goroutine将消息发送到c1,将导致许多消息被发送到c1而没有for-select语句接收。这就导致了一个死锁,因为当发送者准备好时,接收者还没有准备好。

如何使select成为非阻塞式的

select语句默认是阻塞的。我们如何使其成为非阻塞的呢?这很简单--我们只需添加一个 default case。

复制代码
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func main() {
    ashleyMsg := make(chan string)
    brianMsg := make(chan string)
    exit := make(chan struct{})
    rand.Seed(time.Now().UnixNano())
    var wg sync.WaitGroup

    go func() {
        numJob := rand.Intn(10)
        fmt.Println("number of jobs:", numJob)
        for i := 0; i < numJob; i++ {
            wg.Add(2)
            go func() {
                defer wg.Done()
                time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond)
                ashleyMsg <- "hi"
            }()
            go func() {
                defer wg.Done()
                time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond)
                brianMsg <- "what's up"
            }()
        }
        wg.Wait()
        close(exit)
    }()

    for {
        select {
        case res1 := <-ashleyMsg:
            fmt.Println("ashley:", res1)
        case res2 := <-brianMsg:
            fmt.Println("brian:", res2)
        case <-exit:
            fmt.Println("chat ended")
            return
        default:
            fmt.Println("...")
            time.Sleep(time.Millisecond)
        }
    }
}

...
number of jobs: 4
brian: what's up
...
ashley: hi
...
...
brian: what's up
ashley: hi
ashley: hi
brian: what's up
...
...
ashley: hi
...
brian: what's up
...
chat ended

除了蹩脚的对话之外,我们可以看到默认情况下的工作方式。与其等待聊天记录的到来,我们可以在没有频道可以接收的时候做一些事情。在这个例子中,我们只是打印出省略号,但你可以做任何你想做的事情。

相关推荐
李广坤18 小时前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库
爱可生开源社区2 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1772 天前
《从零搭建NestJS项目》
数据库·typescript
花酒锄作田2 天前
Gin 框架中的规范响应格式设计与实现
golang·gin
加号33 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏3 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐3 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再3 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip
tryCbest3 天前
数据库SQL学习
数据库·sql
jnrjian3 天前
ORA-01017 查找机器名 用户名 以及library cache lock 参数含义
数据库·oracle