0

我使用 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 的 Denom() 源代码用于 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 时执行以表示一个值,或者在 Rat 中设置一个新值。我担心的是一次又一次地重复计算相同的值(不是并发安全的),除非由 Rat 表示的值被更改。那为什么不能在创建/修改部分完成呢?

4

2 回答 2

1

您有一个明确的竞争条件,简而言之,竞争条件是当两个以上的异步例程(线程、进程、协同例程等)试图访问(写入或读取)资源(内存或 i/o 设备)时并且至少其中一个例程有编写的意图。

在您的情况下,您正在创建访问共享资源(var x Rat)的 goroutine,并且您可能会注意到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 -race rat.go

事实上,你会看到你的代码和我的解决方案之间的区别只是使用-race

于 2016-10-25T18:28:52.290 回答
0

Rat.Denom正如@JimB 在评论中指出的那样,似乎不是线程安全的。标准库的一般规则是方法不是线程安全的,除非明确指出。

您的另一个问题是闭环的常见陷阱。

有关该问题的描述,请参阅本文。您必须将变量传递到闭包中,如下所示。

工作示例:

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以获取实时示例。

于 2016-10-25T15:56:34.847 回答