我使用math / big.Rat表示数字以确保准确性。
Denom()返回数字的分母,而Cmp()返回两个数字的分母。它们似乎都是纯只读功能。但是,当我在启用数据竞争的情况下运行代码时,我的整个假设都错了。当这些函数与同一个Rat实例并发调用时,系统将引发数据争用情况。这些功能不是只读的吗?
我的测试用例
package main
import (
"math/big"
"sync"
)
func main() {
x := big.NewRat(5, 1)
wg := new(sync.WaitGroup)
// just for testing
for i := 0; i < 10; i++ {
go func() {
wg.Add(1)
defer wg.Done()
if i%2 == 0 {
x.Cmp(x)
} else {
x.Denom()
}
}()
}
wg.Wait()
}
当我检查源代码时,每次调用Denom()函数时,它将在同一对象中重置值。这是源代码中的问题吗?否则我不应该同时使用Rat Denom()和Cmp()。
Denom()源于Golang for ref。
// Denom returns the denominator of x; it is always > 0.
400 // The result is a reference to x's denominator; it
401 // may change if a new value is assigned to x, and vice versa.
402 func (x *Rat) Denom() *Int {
403 x.b.neg = false // the result is always >= 0
404 if len(x.b.abs) == 0 {
405 x.b.abs = x.b.abs.set(natOne) // materialize denominator
406 }
407 return &x.b
408 }
我将在下面的讨论基础上再增加一些观点,并且我接受我在将变量'i'用于预期目的时犯了一个错误(但是仍然可以显示数据竞争情况)。
我的意思是在Denom()中执行的操作不会修改Rat表示的值。这可以在创建大鼠以表示一个值时执行,也可以在大鼠中设置一个新值。我担心的是一次又一次地重复计算(不是同时安全的)相同值,除非更改了Rat表示的值。那为什么不能在创建/修改部分完成呢?
最佳答案
您有明确的竞争条件,简而言之,竞争条件是当两个以上的异步例程(线程,进程,协同例程等)试图访问(写入或读取)资源(内存或I / O设备)时这些例程中至少有一个打算编写。
在您的情况下,您正在创建访问共享资源( var x Rat )的goroutines,并且您可能会注意到方法 Denom()会在403和405行修改其自身的值,并且似乎Cmp()方法只是阅读。我们要做的就是使用RWMutex保护内存:
package main
import (
"math/big"
"sync"
)
func main() {
x := big.NewRat(5, 1)
wg := new(sync.WaitGroup)
mutex := new(sync.RWMutex)
wg.Add(10) // all goroutines
for i := 0; i < 10; i++ {
go func(index int) {
defer wg.Done()
if index%2 == 0 {
mutex.RLock() // locks only for reading
x.Cmp(x)
mutex.RUnlock() // unlocks for reading
} else {
mutex.Lock() // locks for writing
x.Denom()
mutex.Unlock() // unlock for writing
}
}(i)
}
wg.Wait()
}
请注意,我们使用RLock和RUnlock进行读取操作,并使用Lock和Unlock()进行写入。另外,如果您知道要创建的goroutine的数量,我总是建议仅在一行中执行
wg.Add(n)
,因为如果在wg.Add(1)之后执行go func(){...},您会遇到麻烦。确实,您确实存在一个常见错误,那就是在goroutine中使用作为索引,始终将它们作为参数传递。
最后,我建议您使用 -race 标志来来运行或来构建命令,例如:
go run -race rat.go
实际上,仅使用 -race ,您就会看到代码和我的解决方案之间的区别