入队和出队会导致通道死锁。

huangapple go评论72阅读模式
英文:

Enque and deque deadlocks the channel

问题

我正在尝试实现一个队列,在单个通道中进行出队和重新入队。

我有两个问题:

  • 为什么会出现死锁?我原本期望会有一个无限循环(因为我在重新入队时甚至生成了更多的元素)。难道 range queue 不应该始终监听通道吗?

  • 这是死锁之前的打印部分:

    enqueueing 1000 enqueueing 1001 dequeued 1001 dequeued 1001 using 1001 using 1001

两个不同的 goroutine 是否在出队相同的元素?我不明白为什么会出现数据竞争;我原以为 range 会一次选择一个元素。

Playground 中的代码

func main() {

	queue := make(chan int)
	start := 10

	go func() { queue <- start }()

	for element := range queue {
		fmt.Println("dequeued ", element)
		go enqueue(element, queue)
	}
}

func enqueue(element int, queue chan int) {
	fmt.Println("using ", element)
	if element%2 == 0 {
		fmt.Println("creating new elements from ", element)
		var news = []int{element * 100, element*100 + 1}

		for _, new := range news {
			fmt.Println("enqueueing ", new)
			go func() { queue <- new }()
		}

	}
}
英文:

I am trying to implement a queue, dequeueing and requeueing in a single channel.

I have two questions:

  • why do I obtain a deadlock? I was expecting an infinite loop (since I am requeueing even elements which generate more elements and so on). Shouldn't the range queue always listening to the channel?

  • this is part of the print before the deadloks:

    enqueueing 1000
    enqueueing 1001
    dequeued 1001
    dequeued 1001
    using 1001
    using 1001

Are two different goroutines dequeueing the same element? I don't understand why this data race; I thought that the range would pick one per time.

Code in Playground

func main() {

	queue := make(chan int)
	start := 10

	go func() { queue &lt;- start }()

	for element := range queue {
		fmt.Println(&quot;dequeued &quot;, element)
		go enqueue(element, queue)
	}
}

func enqueue(element int, queue chan int) {
	fmt.Println(&quot;using &quot;, element)
	if element%2 == 0 {
		fmt.Println(&quot;creating new elements from &quot;, element)
		var news = []int{element * 100, element*100 + 1}

		for _, new := range news {
			fmt.Println(&quot;enqueueing &quot;, new)
			go func() { queue &lt;- new }()
		}

	}
}

答案1

得分: 4

这是一个作用域问题。当使用go func() {...}开始时,这种情况经常发生;它可能比并发的真正问题更常见。关于这个问题,在FAQGo wiki中有相关的部分。

你创建的匿名func获取的是外部作用域中变量的引用,而不是在func语句运行时的。而且,循环变量在每次循环中更新,所以在你的go语句和goroutine实际运行之间会发生变化。

你可以通过在每次循环迭代期间声明另一个变量来解决这个问题(即在for括号内部)。如果你的循环是for i := range arr {...},你可以添加i := i。所以,这个错误的版本:

arr := make([]int, 10)
for i := range arr {
    go func() { 
        fmt.Println(i)
    }()
}

...总是打印9。修复后的版本,在循环内部重新声明i

arr := make([]int, 10)
for i := range arr {
    i := i
    go func() { 
        fmt.Println(i)
    }()
}

...打印0-9。另一种不同但更优雅的重新声明i的方式是将其作为匿名func的参数;这样它就不是一个看起来奇怪的独立语句了:

arr := make([]int, 10)
for i := range arr {
    go func(i int) { 
        fmt.Println(i)
    }(i)
}

这是那段代码。对于所有的Playground版本,我都必须添加同步,以防止main在goroutine运行之前退出。

这个声明在每次循环中“运行”一次,行为不同(作用域的奇怪表现方式),但解决方法是相当直接的。


在你的情况下:queue <- new可以在任何时候运行,事实证明它是在你完全遍历for _, new循环之后运行的。然而,它使用的是new的值在它实际运行时的值,而不是在go语句执行时的值。在这种情况下,你启动的两个goroutine都得到了值1001,所以第二次循环中传递给enqueue的两个值都是奇数(你可以在输出中看到两个using 1001),所以没有任何内容写入队列,所以range queue循环没有消费任何内容。通道也没有关闭,所以main不能简单地结束,所以你会遇到死锁。

你希望为每个goroutine的执行捕获一个不同的值。为此,你可以在循环的顶部放置new := new,尽管看起来有点奇怪。这足以使每次迭代的值从Go的角度来看成为一个“不同的var”,所以你会在通道中插入10001001

一旦你真正让它工作起来,Playground实际上不会成功运行你的代码,因为它会无限循环并且有大量的goroutine在运行,我猜Playground不喜欢这样(过度使用资源)。如果你在退出之前添加了100个元素的限制,你会得到http://play.golang.org/p/bBM3uTnvxi。你还会注意到输出的数字变得奇怪,因为每次乘以100最终会溢出机器的int类型,但这只是在使用低级语言编写程序时的情况。

作为一个小事,你可能不想将变量命名为new,因为那也是一个内置函数的名称。虽然这样做是合法的,但会令人困惑。(如果你从其他语言过来,将长度变量命名为len可能有点反射性。)

英文:

It's a scope problem. This happens a lot when getting started with go func() {...}; it might be more common than real problems with concurrency. There are sections in the FAQ and the Go wiki about it.

The anonymous func you create gets a reference to a variable in the outer scope, not to its value at the time the func statement runs. And that loop variable is updated each pass through the loop, so it changes between your go statement and when the goroutine actually runs.

You can work around this by declaring another variable during each iteration of the loop (i.e., inside the for braces). If your loop was for i := range arr {...} you could just add i := i. So, this bad version:

arr := make([]int, 10)
for i := range arr {
	go func() { 
		fmt.Println(i)
    }()
}

...always prints 9. A fixed version, redeclaring i inside the loop:

arr := make([]int, 10)
for i := range arr {
    i := i
	go func() { 
		fmt.Println(i)
    }()
}

...prints 0-9. A different, arguably more elegant way to redeclare i is to make it a param of the anonymous func; then it's not a weird-looking standalone statement:

arr := make([]int, 10)
for i := range arr {
	go func(i int) { 
		fmt.Println(i)
    }(i)
}

Here's that code. For all of the Playground versions, I had to add synchronization so main wouldn't exit before the goroutines run.

It's confusing that a declaration that "runs" once each time through the loop behaves differently (weird way for scope to manifest) but the way to get around it is straightforward enough.


In your case: queue &lt;- new can run at any time, and it turns out to run after you've gone through the for _, new loop entirely. However, it uses whatever the value of new is as of when it actually runs, not as of when the go statement was executed. In this case, both the goroutines you start get the value 1001, the so second time through both values passed into enqueue are odd (you can see that as two using 1001s in your output) so nothing writes to the queue, so there is nothing for the range queue loop to consume. The channel isn't closed either, so the main can't just end, so you get a deadlock.

You want a different value to be "captured" for each execution of the goroutine. For that, you can put new := new at the top of the loop, as funny as that looks. That's enough to make the value from each iteration a "different var" from Go's perspective, so you get 1000 and 1001 inserted in the channel.

Once you've actually got it working, the Playground won't actually run your code successfully because it loops forever and there are a zillion goroutines running and I guess the Playground doesn't like that (overusing resources). If you add a limit of 100 elements dequeued before quitting, you get http://play.golang.org/p/bBM3uTnvxi . You'll also notice the numbers it outputs get weird because multiplying by 100 each time will eventually overflow the machine's int type, but that's just how it goes writing programs in lowish-level languages.

As a minor thing, you probably don't want to name a variable new because that's also the name of a built-in function. It's legal to do so, just confusing. (It can be kind of reflexive to name a length variable len, especially if nothing was wrong with that in the language you're coming from.)

答案2

得分: 0

为什么我会遇到死锁?我原本期望是一个无限循环(因为我在重新排队生成更多元素的偶数元素)。难道 range 循环不应该始终监听通道吗?

当一个 range 循环遍历通道时,如果达到了通道缓冲区的末尾(如果通道有缓冲区),它将被阻塞,等待更多的元素通过通道发送。如果没有其他正在运行或可运行的 goroutine,那么就会发生死锁。

最简单的示例:

package main

func main() {

    queue := make(chan int)

    for _ = range queue {
    }
}

两个不同的 goroutine 是否在出队同一个元素?

不是的。发生的情况是这一行(第26行):

go func() { queue <- new }()

告诉 Go 的运行时调度一个新的 goroutine,但不一定立即执行。当 goroutine 实际运行时,new 的值是1001,而不是打印的1000。

英文:

> why do I obtain a deadlock? I was expecting an infinite loop (since I am requeueing even elements which generate more elements and so on). Shouldn't the range queue always listening to the channel?

When a range loop over a channel reaches the end of the channel's buffer (if the channel has one) it will block, waiting for more elements to be sent through the channel. If there are no other running or runnable goroutines then we've reached a deadlock.

Minimal example:

http://play.golang.org/p/Vb4-RFEmm3

package main

func main() {

	queue := make(chan int)

	for _ = range queue {
	}
}

> Are two different goroutines dequeueing the same element?

No. What's happening is that this line (26):

go func() { queue &lt;- new }()

instructs Go's runtime to schedule a new goroutine, it does not necessarily execute immediately. When the goroutine effectively runs, new's value is 1001, not 1000 as printed.

huangapple
  • 本文由 发表于 2014年9月28日 07:51:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/26080263.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定