我使用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 ,您就会看到代码和我的解决方案之间的区别

09-28 08:37