How did I confuse scopes of variables and pointers in goroutines here?

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

How did I confuse scopes of variables and pointers in goroutines here?

问题

我正在通过编写一个简单的程序来学习Go语言,该程序可以并发地从几个HTTP服务器下载传感器数据文件。服务器上的传感器数据文件会定期刷新(30秒或2分钟,取决于'origin')。下载数据的时间可能从100毫秒到10秒不等。因此,我为每个服务器(OriginContext)读取了一些配置。然后,我为每个OriginContext启动一个控制器。每个控制器都会连续触发一个goroutine来执行下载等操作。

我将代码简化为一个最小示例,希望能够显示我的意图。当我运行它时,会有两个控制器,但是当它们触发doStuffThatMayTakeLongTime()方法时,它们都引用相同的配置。

那么,在这里我是如何混淆变量和指针的作用域的呢?

我对Go语言非常陌生,这也是我第一次尝试使用指针的语言。嗯,我对C/C++的尝试已经超过十年了...所以我认为我对引用/值/解引用感到困惑,但我看不出来。

这是代码:

package main

import (
	"log"
	"time"
)

type OriginContext struct {
	Origin   string
	Offset   time.Duration
	Interval time.Duration
}

type Controller struct {
	originContext *OriginContext
}

func NewController(originContext *OriginContext) (w *Controller) {
	log.Printf("Controller starting loop for origin %s.", originContext.Origin)
	w = &Controller{originContext}
	w.start()
	return w
}

func (w *Controller) start() {
	log.Println("start() of", w.originContext.Origin)
	go func() {
		time.Sleep(w.originContext.Offset)
		ticker := time.NewTicker(w.originContext.Interval)
		go w.doStuffThatMayTakeLongTime() // iteration zero
		for {
			select {
			case <-ticker.C:
				go w.doStuffThatMayTakeLongTime()
			}
		}
	}()
}

func (w *Controller) doStuffThatMayTakeLongTime() {
	log.Printf("%s doing stuff", w.originContext.Origin)
}

func main() {
	contexts := []OriginContext{
		{
			Origin:   "alpha",
			Offset:   0 * time.Second,
			Interval: 5 * time.Second,
		},
		{
			Origin:   "bravo",
			Offset:   5 * time.Second,
			Interval: 10 * time.Second,
		},
	}
	for _, ctx := range contexts {
		log.Printf("Starting Controller %s.", ctx.Origin)
		_ = NewController(&ctx)
	}
	select {}
}

以下是一些输出:

2015/09/07 14:30:11 Starting Controller alpha.
2015/09/07 14:30:11 Controller starting loop for origin alpha.
2015/09/07 14:30:11 start() of alpha
2015/09/07 14:30:11 Starting Controller bravo.
2015/09/07 14:30:11 Controller starting loop for origin bravo.
2015/09/07 14:30:11 start() of bravo
2015/09/07 14:30:16 bravo doing stuff
2015/09/07 14:30:16 bravo doing stuff
2015/09/07 14:30:26 bravo doing stuff
2015/09/07 14:30:26 bravo doing stuff

应该有alpha和bravo都在"doing stuff",但实际上只有bravo。

英文:

I am learning Go by writing a simple program that concurrently downloads sensor data files from a few http servers. The sensor data files on the servers are refreshed at regular intervals (30 seconds or 2 minutes, depends on the 'origin'). Downloading the data can take from 100ms to 10 seconds. So I read some configurations for each server (OriginContext). Then I start a controller for each OriginContext. Each controller continuously fires a goroutine that does the download etc.

I stripped my code down to a minimal example that somehow/hopefully still shows my intentions. When I run it, there will be two controllers, but somehow when they fire the doStuffThatMayTakeLongTime() methods they all refer to the identical configuration.

So, how did I confuse scopes of variables and pointers in goroutines here?

I am very new to Go and also this is the first time I try to use a language that uses pointers. Well, my shy C/C++ attempts are more than a decade ago... so I assume my confusion is with reference/value/dereference, but I can't see it.

This is the code:

package main
import (
&quot;log&quot;
&quot;time&quot;
)
type OriginContext struct {
Origin   string
Offset   time.Duration
Interval time.Duration
}
type Controller struct {
originContext *OriginContext
}
func NewController(originContext *OriginContext) (w *Controller) {
log.Printf(&quot;Controller starting loop for origin %s.&quot;, originContext.Origin)
w = &amp;Controller{originContext}
w.start()
return w
}
func (w *Controller) start() {
log.Println(&quot;start() of&quot;, w.originContext.Origin)
go func() {
time.Sleep(w.originContext.Offset)
ticker := time.NewTicker(w.originContext.Interval)
go w.doStuffThatMayTakeLongTime() // iteration zero
for {
select {
case &lt;-ticker.C:
go w.doStuffThatMayTakeLongTime()
}
}
}()
}
func (w *Controller) doStuffThatMayTakeLongTime() {
log.Printf(&quot;%s doing stuff&quot;, w.originContext.Origin)
}
func main() {
contexts := []OriginContext{
{
Origin:   &quot;alpha&quot;,
Offset:   0 * time.Second,
Interval: 5 * time.Second,
},
{
Origin:   &quot;bravo&quot;,
Offset:   5 * time.Second,
Interval: 10 * time.Second,
},
}
for _, ctx := range contexts {
log.Printf(&quot;Starting Controller %s.&quot;, ctx.Origin)
_ = NewController(&amp;ctx)
}
select {}
}

And this is some output:

2015/09/07 14:30:11 Starting Controller alpha.
2015/09/07 14:30:11 Controller starting loop for origin alpha.
2015/09/07 14:30:11 start() of alpha
2015/09/07 14:30:11 Starting Controller bravo.
2015/09/07 14:30:11 Controller starting loop for origin bravo.
2015/09/07 14:30:11 start() of bravo
2015/09/07 14:30:16 bravo doing stuff
2015/09/07 14:30:16 bravo doing stuff
2015/09/07 14:30:26 bravo doing stuff
2015/09/07 14:30:26 bravo doing stuff

There should be alpha and bravo doing stuff, but there is just bravo.

答案1

得分: 4

问题出在以下代码行:

    for _, ctx := range contexts {
log.Printf("Starting Controller %s.", ctx.Origin)
_ = NewController(&ctx)
}

变量ctx在循环的每次迭代中被重用(如语言规范所述)。NewController在循环的每次迭代中都会传递该单个变量的地址。程序打印的是该变量中存储的最后一个值(尽管这并不保证,因为该变量存在竞争)。

<kbd>运行打印 &ctx 的示例</kbd>

有几种修复方法。一种方法是将代码更改为:

for i := range contexts {
log.Printf("Starting Controller %s.", context[i].Origin)
_ = NewController(&context[i])
}

<kbd>在 playground 上运行它</kbd>

通过这种更改,NewController接收的是指向切片元素的指针,而不是函数中的变量的指针。

另一种选择是在循环体内部声明一个新变量:

    for _, ctx := range contexts {
ctx := ctx // &lt;-- 添加这一行
log.Printf("Starting Controller %s.", ctx.Origin)
_ = NewController(&ctx)
}

<kbd>在 playground 上运行它</kbd>

这种选项在每次循环迭代时都会分配一个新的ctx变量,而第一种选项则不会。

英文:

The problem is on these lines:

    for _, ctx := range contexts {
log.Printf(&quot;Starting Controller %s.&quot;, ctx.Origin)
_ = NewController(&amp;ctx)
}

The variable ctx is reused on every iteration of the loop as described in the language specification. NewController is passed address of this single variable on every iteration of loop. The program prints the last values stored in this variable (although that's not guaranteed, there's a race on the variable).

<kbd>run example that prints &ctx</kbd>

There are a few ways to fix this. One way is to change the code to:

for i := range contexts {
log.Printf(&quot;Starting Controller %s.&quot;, context[i].Origin)
_ = NewController(&amp;context[i])
}

<kbd>run it on the playground</kbd>

With this change, NewController is passed a pointer to the slice element instead of a pointer to variable in the function.

Another option is to declare a new variable inside the body of the loop:

    for _, ctx := range contexts {
ctx := ctx // &lt;-- add this line
log.Printf(&quot;Starting Controller %s.&quot;, ctx.Origin)
_ = NewController(&amp;ctx)
}

<kbd>run it on the playground</kbd>

This option allocates a ctx on every iteration through the loop while the first option does not.

huangapple
  • 本文由 发表于 2015年9月7日 20:38:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/32438981.html
匿名

发表评论

匿名网友

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

确定