select - 当涉及多个 channel 时,select 如何工作?

标签 select go scheduling channel goroutine

我发现在多个非缓冲 channel 上使用 select 时,例如

select {
case <- chana:
case <- chanb:
}

即使两个 channel 都有数据,但是在处理这个选择时, case chana 和 case chanb 的调用不平衡。

package main

import (
    "fmt"
    _ "net/http/pprof"
    "sync"
    "time"
)

func main() {
    chana := make(chan int)
    chanb := make(chan int)

    go func() {
        for i := 0; i < 1000; i++ {
            chana <- 100 * i
        }
    }()

    go func() {
        for i := 0; i < 1000; i++ {
            chanb <- i
        }
    }()

    time.Sleep(time.Microsecond * 300)

    acount := 0
    bcount := 0
    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() {
        for {
            select {
            case <-chana:
                acount++
            case <-chanb:
                bcount++
            }
            if acount == 1000 || bcount == 1000 {
                fmt.Println("finish one acount, bcount", acount, bcount)
                break
            }
        }
        wg.Done()
    }()

    wg.Wait()
}

运行这个demo,当其中一个chana,chanb完成读/写时,另一个可能还剩下999-1。

有什么方法可以保证平衡吗?

找到相关主题
golang-channels-select-statement

最佳答案

Go select 语句不偏向于任何(就绪)情况。引用规范:

If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the "select" statement blocks until at least one of the communications can proceed.

如果可以进行多个通信,则随机选择一个。这不是一个完美的随机分布,规范也不能保证这一点,但它是随机的。

您所体验到的是 Go Playground 具有 GOMAXPROCS=1 ( which you can verify here ) 且 goroutine 调度程序未被抢占的结果。这意味着默认情况下 goroutines 不是并行执行的。如果遇到阻塞操作(例如从网络读取,或尝试从阻塞的 channel 接收或发送),一个 goroutine 将被置于公园,另一个准备运行的 goroutine 继续。

并且由于您的代码中没有阻塞操作,因此可能不会将 goroutines 放在 park 中,并且可能只有一个“生产者”goroutines 会运行,而另一个可能不会(永远)被调度。

在我的本地计算机上运行您的代码,其中 GOMAXPROCS=4,我得到了非常“真实”的结果。运行几次,输出:

finish one acount, bcount 1000 901
finish one acount, bcount 1000 335
finish one acount, bcount 1000 872
finish one acount, bcount 427 1000

如果您需要对单个案例进行优先排序,请查看此答案:Force priority of go select statement

select 的默认行为不保证相等的优先级,但平均而言它会接近它。如果您需要保证相同的优先级,则不应使用 select,但您可以从 2 个 channel 执行一系列 2 个非阻塞接收,看起来像这样:

for {
    select {
    case <-chana:
        acount++
    default:
    }
    select {
    case <-chanb:
        bcount++
    default:
    }
    if acount == 1000 || bcount == 1000 {
        fmt.Println("finish one acount, bcount", acount, bcount)
        break
    }
}

如果两个都提供值,上述 2 个非阻塞接收将以相同的速度(具有相同的优先级)耗尽 2 个 channel ,如果一个不提供,则不断接收另一个,而不会延迟或阻塞。

对此要注意的一件事是,如果没有 channel 提供任何接收值,这基本上是一个“繁忙”循环,因此会消耗计算能力。为避免这种情况,我们可能会检测到没有任何 channel 准备就绪,然后然后对两个接收方使用select 语句,然后它将阻塞直到其中一个准备就绪准备接收,不浪费任何 CPU 资源:

for {
    received := 0
    select {
    case <-chana:
        acount++
        received++
    default:
    }
    select {
    case <-chanb:
        bcount++
        received++
    default:
    }

    if received == 0 {
        select {
        case <-chana:
            acount++
        case <-chanb:
            bcount++
        }
    }

    if acount == 1000 || bcount == 1000 {
        fmt.Println("finish one acount, bcount", acount, bcount)
        break
    }
}

有关 goroutine 调度的更多详细信息,请参阅以下问题:

Number of threads used by Go runtime

Goroutines 8kb and windows OS thread 1 mb

Why does it not create many threads when many goroutines are blocked in writing file in golang?

关于select - 当涉及多个 channel 时,select 如何工作?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47645808/

相关文章:

c++ - I2C 文件描述符上的 I2C 总线可写/可读标志

c# - NHibernate 根据非主键列从表中选择数据

Go 不接收错误会触发 panic ,但接收错误不会触发 panic

go - 如何使用可变参数包装函数

c# - 在 C# 中处理 TCP 服务器中的用户超时

mysql - 如何获得mysql中某天之前和某天之后的平均评分?

mysql - 如何连接两个表,保留不满足 JOIN 条件的行?

mongodb - 使用 golang 和 mongodb 进行顺序查询

java - Spring 4.0.2中如何在@Scheduled方法中获取当前 session (HttpSession)对象?

c# - 如何在 Quartz 调度程序中每 3 分钟运行一次?