最近在学习golang,记一次goroutine传参引发的计算结果和预期不一致问题。
例子是一个多goroutine并行计算一个数的值,一个task的chan,和一个result的chan用来存储分配的每个任务,和每个任务的计算结果,最后将所有的任务结果相加
先看有问题的代码:
package main
import (
"fmt"
"sync"
)
//利用chan将计算任务拆分成多个task,并发计算
type task struct {
begin int
end int
result chan int
}
func (t *task) do() {
sum := 0
for i := t.begin; i <= t.end; i++ {
sum = sum + i
}
fmt.Printf("this sum is %d\n", sum)
t.result <- sum
}
//分配task,将总的任务分割
func InitTask(taskchan chan task, r chan int, count int) {
//10个任务,每个任务的数目
cp := count / 10
//余数
du := count % 10
high := du + cp*10
for j := 0; j < 10; j++ {
var b int
var e int
if j == 9 && du != 0 {
b = cp*j + 1
e = high
} else {
b = cp*j + 1
e = cp * (j + 1)
}
fmt.Printf("b is %d, e is %d\n", b, e)
tak := task{
begin: b,
end: e,
result: r,
}
taskchan <- tak
}
//切记关闭chan,否则会死锁
close(taskchan)
}
func DisbuteTask(tak chan task, result chan int, wg *sync.WaitGroup) {
for v := range tak {
fmt.Println("开始一个计算")
wg.Add(1)
//注意,这里直接使用了函数外的v变量
go func() {
defer wg.Done()
v.do()
fmt.Println("结束一个计算")
}()
}
要等待每个task运行完 关闭通道,这句换个地方会导致结果出错
wg.Wait()
fmt.Println("关闭计算结果通道")
//切记关闭chan,否则会死锁
close(result)
}
func main() {
//任务
taskchan := make(chan task, 10)
//结果
resultchan := make(chan int, 10)
var wg sync.WaitGroup
go InitTask(taskchan, resultchan, 118)
//计算每个task的sum
go DisbuteTask(taskchan, resultchan, &wg)
sum := ProcessResult(resultchan)
println(sum)
}
func ProcessResult(result chan int) int {
sum := 0
for v := range result {
sum = sum + v
}
return sum
}
执行结果
与预期结果不一致,而且每次执行的结果都不一样,这可把我整迷糊了,反复看了代码,逻辑没什么问题呀,折腾了老半天,打印了许多日志,发现了一个有意思的现象:
这里竟然两次计算结果是一样的,但是分割的日志显示是正确的
到这里都是正常的,那问题一定出在DisbuteTask()这个函数,我反复看了一下这个函数,导致两次计算结果一样那必定是两次传入do函数的task的begin和end值是一样的,什么情况下才会发生这种情况?
在观察下这个函数,这里每个task结构调用了do方法,那么只有一种解释:两次循环传入了同一个v结构体,那么肯定是当进入for循环时值是正确的,然后当开启一个goroutine的时候v的值被动态刷新了,导致了结果的出乎意料。
再来看一下这个函数:
func DisbuteTask(tak chan task, result chan int, wg *sync.WaitGroup) {
for v := range tak {
fmt.Println("开始一个计算")
wg.Add(1)
go func() {
defer wg.Done()
v.do()
fmt.Println("结束一个计算")
}()
}
wg.Wait()
fmt.Println("关闭计算结果通道")
//切记关闭chan,否则会死锁
close(result)
}
那么,正确的代码应该是:
func DisbuteTask(tak chan task, result chan int, wg *sync.WaitGroup) {
for v := range tak {
fmt.Println("开始一个计算")
wg.Add(1)
go func(v task) {
defer wg.Done()
v.do()
fmt.Println("结束一个计算")
}(v)
}
wg.Wait()
fmt.Println("关闭计算结果通道")
//切记关闭chan,否则会死锁
close(result)
}
在启动一个goroutine的时候将v结构体显式的传入函数,最后的结果就对了!