【问题标题】:Get deadlock when try to received value twice尝试两次接收值时出现死锁
【发布时间】:2014-11-18 12:14:23
【问题描述】:

我观看了关于 Advanced Go Concurrency Patterns 的精彩视频。 Sameer Ajmani 一开始展示了一个乒乓球应用程序。

package main

import (
    "fmt"
    "time"
)

type Ball struct{ hits int }

func main() {
    table := make(chan *Ball)
    go player("ping", table)
    go player("pong", table)

    table <- new(Ball) // game on; toss the ball
    time.Sleep(1 * time.Second)
    fmt.Println(<-table) // game over; grab the ball
}

func player(name string, table chan *Ball) {
    for {
        ball := <-table
        ball.hits++
        fmt.Println(name, ball.hits)
        time.Sleep(100 * time.Millisecond)
        table <- ball
    }
}

代码是如何工作的,我理解了 90%。它们是两个 goroutine,它们在主线程休眠期间相互发送消息,ping 和 pong。

然后我尝试关注

package main

import (
    "fmt"
    "time"
)

type Ball struct{ hits int }

func main() {
    table := make(chan *Ball)
    go player("ping", table)
    go player("pong", table)

    table <- new(Ball) // game on; toss the ball
    time.Sleep(1 * time.Second)
    fmt.Println(<-table) // game over; grab the ball
    fmt.Println(<-table) // game over; grab the ball
}

func player(name string, table chan *Ball) {
    for {
        ball := <-table
        ball.hits++
        fmt.Println(name, ball.hits)
        time.Sleep(100 * time.Millisecond)
        table <- ball
    }
}

我在这里陷入僵局,真的不明白为什么。查看 go 例程中的最后一行,我尝试像倒数第二行一样从通道接收值。在后台,两个 goroutine 仍然继续循环并相互发送值。对我来说,它似乎是一个用于表变量通道的多个接收器。

我的主要问题是,我从第二个样本中得到了什么死锁?

【问题讨论】:

  • 不要抢球。关闭频道就行了。要检查该通道是否已关闭,请使用 x, ok := play.golang.org/p/N579duQZOg

标签: multithreading go


【解决方案1】:

在后台,两个 goroutine 仍然继续循环并相互发送值。

不,他们没有。

当您使用make(chan *Ball) 创建频道时,您正在创建一个无缓冲频道。这相当于说make(chan *Ball,0),这意味着通道可以在其中容纳 0 个东西 - 或者更明确地说,对通道的任何写入都会阻塞,直到另一个例程从通道读取,反之亦然。

无缓冲通道的执行顺序如下:

  • 玩家“ping”已创建,尝试从带有ball := &lt;-table 的表中读取,被阻止直到table 被写入
  • 玩家“pong”已创建,尝试使用ball := &lt;-table 从表中读取,被阻止直到table 被写入
  • Main 使用以下行将 Ball 写入 table

     table <- new(Ball) // game on; toss the ball
    

    这不是因为有人在频道上等待阅读而被阻止。

  • 现在 ping 读取球(在 ping 的 ball := &lt;-table 之后继续执行)
  • Ping 用table &lt;- ball 将球放在桌子上,因为 pong 正在等待,所以没有被挡住
  • Pong 读球(在 pong 的ball := &lt;-table 之后继续执行)
  • Pong 用table &lt;- ball 将球放在桌子上,没有被阻止,因为 ping 正在等待
  • Ping 读球(在 ping 的ball := &lt;-table 之后继续执行)
  • ....等
    • 直到主要通过阅读球而不是其中一名球员结束比赛

这是使用通道确保一次只运行一个例程的一个很好的例子。

为了结束游戏,主线程在一秒钟后将球抢出通道:

 time.Sleep(1 * time.Second)
 fmt.Println(<-table) // game over; grab the ball

在此之后,table 通道将是空的,并且任何进一步的读取都会阻塞。

  • 此时,player 的两个例程都在 ball := &lt;- table 处被阻止。

如果您在主线程中进一步进行&lt;-table 读取, 读取也将阻塞,直到例程尝试写入表通道。但是,由于没有其他例程在运行,因此您遇到了死锁(所有 goroutine 都被阻塞)。


好的。那我可以在通道里放两个球吗?

没有。

如果我们尝试将两个球放入通道中,我们可能会得到输出:

Ping 1
Pong 1

因为player 的两个例程都会卡在试图将他们的球放回通道中 - 但没有人会尝试阅读它。顺序如下:

  • 玩家“ping”已创建,尝试从表中读取,被阻止
  • 玩家“pong”已创建,尝试从表中读取,被阻止
  • main 将第一个球放入table(未阻止,因为有人在等待阅读)
  • main 将第二个球放入 table(未阻止,因为有人在等待阅读)
  • Ping 读到第一个球
  • Pong 读第二个球
  • Ping 将第一个球放在桌子上,因为没有人等着看球而被挡住
  • Pong 将第二个球放在桌子上,因为没有人在等待阅读而被挡住
  • 两个玩家都被阻止
    • 直到 main 通过阅读两个球结束比赛。

Here's a program to illustrate

正如评论者指出的那样,结束游戏更好的做法是关闭频道。但是,我希望这次讨论能够消除您的困惑。

【讨论】:

  • 当我删除 the line table &lt;- new(Ball) // game on; toss the ball 时为什么会出现死锁?
  • 再次,您将遇到死锁,因为所有 goroutine 都在等待读取通道(但没有人会写入通道)。我已经编辑了我的答案,希望能更清楚。
  • 主线程从其他goroutine偷球,因为它已经准备好接球了,其他goroutine还在忙着接球对吧?一个发送另一个接收。在节目中,他们是两个潜在的接收者,对吧?
  • 一旦main 的睡眠结束并且player 中的发送被命中,就有两个可能的接收者(main 和另一个player)。我不认为 Go 定义了等待接收者执行的顺序 - 所以其他 player 可能会接收球而不是 main,但是一旦 main 抓住它,@ 就无法进一步执行987654352@例程。
  • 你是 golang 专家。非常感谢您的帮助。阅读频道很容易理解,但实践起来却很难。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-05
  • 1970-01-01
相关资源
最近更新 更多