为什么使用goroutine时有时不会发生竞态条件?

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

Why race condition with goroutine won't happen some time?

问题

我正在阅读《Go语言实战》。这个例子来自第6章的listing09.go。

// 这个示例程序演示了如何在我们的程序中创建竞态条件。我们不希望这样做。
package main

import (
	"fmt"
	"runtime"
	"sync"
)

var (
	// counter是所有goroutine都会增加的变量。
	counter int

	// wg用于等待程序完成。
	wg sync.WaitGroup
)

// main是所有Go程序的入口点。
func main() {
	// 添加两个计数器,每个goroutine一个。
	wg.Add(2)

	// 创建两个goroutine。
	go incCounter(1)
	go incCounter(2)

	// 等待goroutine完成。
	wg.Wait()
	fmt.Println("Final Counter:", counter)
}

// incCounter增加包级别的counter变量。
func incCounter(id int) {
	// 调用Done告诉main我们已经完成。
	defer wg.Done()

	for count := 0; count < 2; count++ {
		// 捕获Counter的值。
		value := counter

		// 让出线程并重新排队。
		runtime.Gosched()

		// 增加我们本地的Counter值。
		value++

		// 将值存回Counter。
		counter = value
	}
}

如果你在play.golang.org上运行这段代码,结果将是2,与书中一样。但是我的Mac大部分时间打印的是4,有时是2,有时甚至是3。

$ go run listing09.go 
Final Counter: 2
$ go run listing09.go 
Final Counter: 4
$ go run listing09.go 
Final Counter: 4
$ go run listing09.go 
Final Counter: 4
$ go run listing09.go 
Final Counter: 4
$ go run listing09.go 
Final Counter: 2
$ go run listing09.go 
Final Counter: 4
$ go run listing09.go 
Final Counter: 2
$ go run listing09.go 
Final Counter: 3

系统信息:

  • go version go1.8.1 darwin/amd64
  • macOS Sierra
  • MacBook Pro

书中的解释(第140页):

每个goroutine都覆盖了另一个的工作。这发生在goroutine交换时。每个goroutine都会创建counter变量的自己的副本,然后被另一个goroutine替换。当goroutine再次执行时,counter变量的值已经改变,但是goroutine不会更新它的副本。相反,它继续递增它所拥有的副本,并将该值设置回counter变量,替换了另一个goroutine执行的工作。

根据这个解释,这段代码应该始终打印2。

  1. 为什么我得到了4和3?这是因为竞态条件没有发生吗?

  2. 为什么play.golang.org总是得到2?


更新:

当我设置runtime.GOMAXPROCS(1)后,它开始打印2,而不是4,有时是3。我猜play.golang.org配置为只有一个逻辑处理器。

正确的结果是4,没有竞态条件。一个逻辑处理器意味着一个线程。默认情况下,Go具有与物理核心相同数量的逻辑处理器。那么,为什么一个线程(一个逻辑处理器)会导致竞态条件,而多个线程打印出正确的答案?

我们可以说书中的解释是错误的吗,因为我们也得到了3和4?它是如何得到3的?4是正确的。

英文:

I'm reading go-in-action. This example is from chapter6/listing09.go.

// This sample program demonstrates how to create race
// conditions in our programs. We don&#39;t want to do this.
package main

import (
  &quot;fmt&quot;
  &quot;runtime&quot;
  &quot;sync&quot;
)

var (
  // counter is a variable incremented by all goroutines.
  counter int

  // wg is used to wait for the program to finish.
  wg sync.WaitGroup
)

// main is the entry point for all Go programs.
func main() {
  // Add a count of two, one for each goroutine.
  wg.Add(2)

  // Create two goroutines.
  go incCounter(1)
  go incCounter(2)

  // Wait for the goroutines to finish.
  wg.Wait()
  fmt.Println(&quot;Final Counter:&quot;, counter)
 }

 // incCounter increments the package level counter variable.
 func incCounter(id int) {
    // Schedule the call to Done to tell main we are done.
    defer wg.Done()

    for count := 0; count &lt; 2; count++ {
	  // Capture the value of Counter.
	  value := counter

	  // Yield the thread and be placed back in queue.
	  runtime.Gosched()

	  // Increment our local value of Counter.
	  value++

	  // Store the value back into Counter.
	  counter = value
  }
}

If you run this code in play.golang.org, it will be 2, same as the book.
But my mac print 4 most of the time, some time 2, some time even 3.

$ go run listing09.go 
Final Counter: 2
$ go run listing09.go 
Final Counter: 4
$ go run listing09.go 
Final Counter: 4
$ go run listing09.go 
Final Counter: 4
$ go run listing09.go 
Final Counter: 4
$ go run listing09.go 
Final Counter: 2
$ go run listing09.go 
Final Counter: 4
$ go run listing09.go 
Final Counter: 2
$ go run listing09.go 
Final Counter: 3

sysinfo
go version go1.8.1 darwin/amd64
macOS sierra
Macbook Pro

Explanation from the book(p140)
> Each goroutine overwrites the work of the other. This happens when the goroutine swap is taking place. Each goroutine makes its own copy of the counter variable and then is swapped out for the other goroutine. When the goroutine is given time to exe- cute again, the value of the counter variable has changed, but the goroutine doesn’t update its copy. Instead it continues to increment the copy it has and set the value back to the counter variable, replacing the work the other goroutine performed.

According to this explanation, this code should always print 2.

  1. Why I got 4 and 3? Is it because race condition didn't happen?

  2. Why go playground always get 2?


update:

After I set runtime.GOMAXPROCS(1), it starts to print 2, no 4, some 3.
I guess the play.golang.org is configured to have one logical processor.

The right result 4 without race condition. One logical processor means one thread. GO has same logical processors as the physical cores by default.So,
why one thread(one logical processor) leads to race condition while multiple thread print the right answer?

Can we say the explanation from the book is wrong since we also get 3 and 4 ?
How it get 3 ? 4 is correct.

答案1

得分: 1

竞态条件是非确定性的。这意味着虽然大多数情况下你可能会得到一个特定的答案,但并不总是如此。

通过在多个核心上运行竞争性代码,你大大增加了可能性的数量,因此你会得到更广泛的结果选择。

有关竞态条件的更多信息,请参阅此帖子此维基百科文章

英文:

Race conditions are, by definition, nondeterministic. This means that while you may get a particular answer most of the time, it will not always be so.

By running racy code on multiple cores you greatly increases the number of possibilities, hence you get a broader selection of results.

See this post or this Wikipedia article for more information on race conditions.

huangapple
  • 本文由 发表于 2017年7月23日 22:47:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/45266393.html
匿名

发表评论

匿名网友

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

确定