【问题标题】:go routine: does select really pick a random case?例行公事:选择真的选择随机案例吗?
【发布时间】:2018-11-30 05:33:57
【问题描述】:

上下文:https://tour.golang.org/concurrency/5

大家好,我正在按照上面的链接学习 Go。

描述说“如果有多个准备好,它会随机选择一个。” 但是,在使主程序等待 2 秒后,在调用 func fibonacci 之前。 2 秒后通道应如下所示: c: 10 次调用以从渠道获取价值 退出:0

在我看来,两个频道都准备好了。如果“如果多个都准备好了,它会随机选择一个”为真,那么斐波那契案例中的第一个调用将有 50% 的机会从退出通道中获得 0。然而,事实并非如此。在退出之前,所有 10 个数字都会被打印出来。因此,看起来选择不是随机的。我错过了什么吗?

package main

import "fmt"
import "time"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    time.Sleep(2 * time.Second)
    fibonacci(c, quit)
}

另外,下一页: https://tour.golang.org/concurrency/6

看起来默认代码应该打印出任何一个勾号。或繁荣!在 500 毫秒。然而,只有BOOM!总是打印出来的。如果我将默认时间从 50 更改为 55,则刻度和 BOOM 都会被打印。为什么是这样?在选择中,After 是否优先于 Tick?

package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(55 * time.Millisecond)
        }
    }
}

【问题讨论】:

  • 为什么你认为两个渠道都准备好了? quit 还没有准备好,因为没有 goroutine 正在写入。
  • 我相信它已经准备好了,因为主例程已经等待了 2 秒,编辑:这应该足以让 go func 例程完成......对吗?
  • 不,goroutine 无法完成,因为它正在等待有人将值写入 c

标签: go select goroutine


【解决方案1】:

我认为您对主要方法中发生的事情有误...为了清楚起见,我将分解我认为发生的事情

func main() {
c := make(chan int)
quit := make(chan int) // make a couple channels
go func() {
    for i := 0; i < 10; i++ {
        fmt.Println(<-c) // blocked here in goroutine
    }
    quit <- 0
}() // go func { ... }() - you're both writing this closure and invoking it as a goroutine
time.Sleep(2 * time.Second) // sleep for 2 seconds
fibonacci(c, quit) // call fibonacci passing in the channels
}

所以这里实际发生的情况是,您已将此闭包称为 goroutine,然后等待 2 秒,在此期间您的 goroutine 仍在 for 循环的主体中等待接收 c,您调用 fibonacci像你期望的那样执行进入for-select,此时你在循环的每次迭代c &lt;- x 上继续点击该代码(它接收,我被递增,你再次接收,下一个值,直到循环结束,因为我== 10)。然后您继续下一行并在退出通道上发送,选择执行该条件并且您的程序退出。

就语言规范所说的首先执行的内容而言;

“选择”语句的执行分几个步骤进行:

1) 对于语句中的所有情况,receive 的通道操作数 操作以及 send 的通道和右侧表达式 语句在输入时按源顺序只计算一次 “选择”语句。结果是一组要接收的通道 from 或 send to,以及要发送的相应值。任何一方 无论哪种情况(如果有),该评估中的影响都会发生 选择通讯操作继续。上的表达 带有短变量声明的 RecvStmt 的左侧或 任务尚未评估。 2) 如果其中一项或多项 通信可以继续,选择一个可以继续的 通过统一的伪随机选择。否则,如果有 默认情况下,选择该情况。如果没有默认情况,则 “选择”语句块,直到至少有一个通信可以 继续。除非所选案例是默认案例,否则相应的 执行通讯操作。 3) 如果所选案例是 带有短变量声明或赋值的 RecvStmt, 评估左侧表达式并接收到值(或 值)被分配。 4) 所选案例的陈述清单为 执行。

在竞态条件下它只是伪随机,问题是你没有创建竞态条件。

【讨论】:

  • 我选择这个作为伟大澄清的答案。谢谢!还要特别感谢用户 "leaf bebop" 指出了 GO 游乐场的怪癖。
  • @PeiWang 是的,np,我试图做一个例子来模拟你正在尝试做的事情,但实际上很难做到,可能想尝试解决这样的事情; play.golang.org/p/efXAPo3iyyf 问题是事件实际上必须同时发生。即使您像在其他变体中那样使用缓冲通道,据我所知,它也不会考虑事件同时发生。所以你实际上必须有两个独立的并行 goroutines 恰好同时发送或者沿着那些不能轻易重新创建的东西
【解决方案2】:

在代码的第一个 sn-p 中,quitfor 循环之前永远不会准备好。而且,在循环的每次迭代中,c 都已准备就绪,并且会阻塞直到发送一个数字。所以select 真的无能为力,只能写信给c。睡两秒也没关系。

第二段代码没有错误。 select 确实随机选择了一个案例。但是,Go Playground 有一个固定的随机生成器,这意味着在 Playground 上,每次运行时 select 总是会选择一个特定的情况。

【讨论】:

    【解决方案3】:

    您创建了一个无缓冲的chan

    c := make(chan int)
    

    这意味着从 chan 读取的任何内容都将被阻塞,直到有内容写入为止。任何写入 chan 的东西都会被阻塞,直到有东西从中读取。

    因此在这段代码中:

        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    

    &lt;-c 会阻塞,直到有东西放在 c 上。所以它就在那里等到你的time.Sleep() 完成之后,然后到达:

        case c <- x:
    

    在此之后,它交替阻塞来回读取一个值,写入一个值,直到它读取 10 个值,然后将 0 发送到 quit

     

    要创建缓冲通道,您需要指定缓冲区的大小。

    c := make(chan int, 10)
    

    但请注意,即使您这样做,它仍然不会像您预期的那样运行,因为您在写入 quit 之前会读取所有 10 个值。你需要把作者放在同一个地方,读者放在同一个地方,而不是把它们混在一起。

    【讨论】:

    • 被缓冲的通道根本没有区别,他的 goroutine 仍然卡在一个循环中,在退出时发送的 mains goroutine 中的代码行可以运行之前重复读取c。跨度>
    • 是的,正如您评论的那样,我正在解决这个问题。其实是两个问题。使用无缓冲的 chan,您将需要 3 个 goroutine。一个 goroutine 使c 准备好(从中读取),一个 goroutine 使 quit 准备好(写入它),一个 goroutine 执行 select。只有 2 个 goroutine,你不能同时做这三个,所以只有一个 chan 会准备好。
    • 不,它与缓冲无关,运行时仍然有排序的概念。查看我刚刚编写的这个示例程序来测试一下; play.golang.org/p/9elo6PtxaGe - 当我的睡眠时间为 100 毫秒时,它每次都会产生正面,如果我让它 1000 毫秒,它每次都会产生反面。如果这被认为是“伪随机”,那么 Go 开发人员需要解决其实现中的错误
    • 那完全是另一回事。这并不能反驳缓冲与非缓冲的东西。 golang 规范只是意味着当对多个就绪通道执行 select 时,您不能依赖任何特定的通道来评估。
    • 是的,当您使用缓冲通道并等待足够长的时间(1 秒,很容易让 goroutine 完成我的发送)时,选择每次都从同一个通道读取,这意味着它根本不是随机的,或者只是在缓冲通道中预加载消息并不意味着运行时决定这些消息同时发生
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-07
    • 2023-03-28
    相关资源
    最近更新 更多