Golang中Goroutine问题的解决方案探讨

Golang中Goroutine问题的解决方案探讨 我正在从《Go实战》这本书中学习goroutine,并运行以下代码:

package main

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

var (
	counter int
	wg sync.WaitGroup
)

func main(){
	wg.Add(2)

	go incCounter(1)
	go incCounter(2)

	wg.Wait()
	fmt.Println("final counter: ", counter)
}

func incCounter(input int){
	defer wg.Done()
	for count := 0; count < 2; count++{
		value := counter
		//do werld thing
		runtime.Gosched()
		value ++
		counter = value
	}
}

由于这两个goroutine在更新包级变量counter时会相互覆盖彼此的工作,所以最终的counter值应该总是2。我尝试运行后发现,大多数时候得到的结果是2,但有时会得到3或4。这是什么原因呢?

(base) zwang-mac:cmd1 zwang$ go run race.go
final counter:  2
(base) zwang-mac:cmd1 zwang$ go run race.go
final counter:  2
(base) zwang-mac:cmd1 zwang$ go run race.go
final counter:  3
(base) zwang-mac:cmd1 zwang$ go run race.go
final counter:  2
(base) zwang-mac:cmd1 zwang$ 

更多关于Golang中Goroutine问题的解决方案探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

感谢您如此详细的解释!作为后续跟进,您认为我可以使用什么可能的解决方案来解决这个数据竞争问题?一般来说,编写多进程的goroutine有哪些最佳实践?

谢谢

更多关于Golang中Goroutine问题的解决方案探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


以下是一个可能的修复示例。它使用互斥锁来确保每次只有一个go例程递增计数器。

func main() {
    fmt.Println("hello world")
}

当你运行它时,计数器将始终为4。

一句话:尽可能详细地规划,直至数据访问层面

当你能够随时回答以下模式时,说明你的规划已步入正轨:

谁在何时通过何种方式执行什么任务

// 代码示例保留原文
func main() {
    fmt.Println("hello world")
}

一句话:尽可能详细地规划,直至数据访问层面。

当你能随时回答以下模式时,说明你的规划已步入正轨:

谁在何时通过何种方式执行什么任务

为了支持他的观点😜,以下是他过往的一些解释:

如何规划

  1. 同步示例问题代码 - hollowaykeanho的第3条回复 - 关于死锁的更多信息
  2. 出现难以理解的死锁 - hollowaykeanho的第4条回复 - 使用伪代码进行规划

这是一个典型的数据竞争(data race)问题。在并发编程中,当多个goroutine同时访问共享变量且至少有一个进行写入操作时,如果没有适当的同步机制,就会出现不可预测的结果。

在你的代码中,问题出现在incCounter函数的这个关键部分:

value := counter     // 读取counter值
runtime.Gosched()    // 主动让出CPU时间片
value++              // 增加本地副本
counter = value      // 写回counter

当两个goroutine并发执行时,可能出现以下竞态条件:

  1. Goroutine 1读取counter值为0
  2. Goroutine 2读取counter值为0
  3. Goroutine 1增加value为1,写回counter=1
  4. Goroutine 2增加value为1,写回counter=1(覆盖了Goroutine 1的结果)
  5. 最终counter只增加了1次而不是2次

但有时也会出现counter=3或4的情况,这是因为在某些执行顺序下,goroutine的读取和写入操作交叉进行,导致某些增加操作被重复计算。

要解决这个问题,需要使用同步原语来保护对共享变量的访问。以下是几种解决方案:

方案1:使用互斥锁(Mutex)

package main

import (
	"fmt"
	"sync"
)

var (
	counter int
	wg      sync.WaitGroup
	mutex   sync.Mutex
)

func main() {
	wg.Add(2)

	go incCounter(1)
	go incCounter(2)

	wg.Wait()
	fmt.Println("final counter: ", counter)
}

func incCounter(input int) {
	defer wg.Done()
	for count := 0; count < 2; count++ {
		mutex.Lock()
		value := counter
		// 这里不再需要runtime.Gosched()
		value++
		counter = value
		mutex.Unlock()
	}
}

方案2:使用原子操作

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var (
	counter int32
	wg      sync.WaitGroup
)

func main() {
	wg.Add(2)

	go incCounter(1)
	go incCounter(2)

	wg.Wait()
	fmt.Println("final counter: ", counter)
}

func incCounter(input int) {
	defer wg.Done()
	for count := 0; count < 2; count++ {
		atomic.AddInt32(&counter, 1)
	}
}

方案3:使用通道(Channel)

package main

import (
	"fmt"
	"sync"
)

var (
	counter int
	wg      sync.WaitGroup
)

func main() {
	wg.Add(2)
	
	// 创建用于同步的通道
	ch := make(chan int, 1)
	ch <- counter // 初始化通道值

	go incCounter(1, ch)
	go incCounter(2, ch)

	wg.Wait()
	
	// 读取最终结果
	final := <-ch
	fmt.Println("final counter: ", final)
}

func incCounter(input int, ch chan int) {
	defer wg.Done()
	for count := 0; count < 2; count++ {
		current := <-ch
		current++
		ch <- current
	}
}

使用go run -race race.go可以检测数据竞争问题,Go的竞态检测器会明确报告存在数据竞争的位置。

回到顶部