我使用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()。
来自Golang for ref的Denom()来源。
// 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 }
我基于下面的讨论增加了几点,我接受我在将varibale'i'用于预期目的时犯了一个错误(但它仍然可以显示数据竞争场景)。
我的观点是Denom()中执行的操作不会对Rat表示的值进行修改。这可以在创建Rat时执行以表示值或在Rat中设置新值。我担心的是,除非Rat所代表的值发生变化,否则反复计算(并非安全)同一值。那为什么不能在创建/修改部分完成呢?
答案 0 :(得分:1)
你有一个明确的竞争条件,简而言之,竞争条件是当两个以上的异步例程(线程,进程,协同例程等)试图访问(写入或读取)资源时(内存或i / o设备)并且至少有一个例程有意写。
在您的情况下,您正在创建访问共享资源的goroutine( var x Rat ),您可能会注意到方法 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中使用 for 索引时会遇到一个常见的错误,总是将它们作为参数传递。
最后,我建议您使用 -race 标记 go run 或 go build 命令,例如:
go run -race rat.go
事实上,只要使用 -race
,您就会看到代码与我的解决方案之间的差异答案 1 :(得分:0)
Rat.Denom
似乎不是线程安全的,正如@JimB在评论中指出的那样。标准库的一般规则是除非明确指出,否则方法不是线程安全的。
你的另一个问题是关闭循环的常见缺陷。
有关该问题的说明,请参阅this article。您必须将变量传递给闭包,如下所示。
工作示例:
package main
import (
"math/big"
"sync"
"fmt"
)
func main() {
x := big.NewRat(5, 1)
wg := new(sync.WaitGroup)
// just for testing
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
fmt.Println("x: ", x)
defer wg.Done()
if i%2 == 0 {
fmt.Printf("i: %d, x.Cmp(x): %+v", i, x.Cmp(x))
} else {
fmt.Println("x.Denom(): ", x.Denom())
}
}(i)
}
wg.Wait()
}
有关实例,请参阅:https://play.golang.org/p/aKo3gHuSeT。