
math/big.rat 的 denom() 和 cmp() 方法并非线程安全——尽管语义上看似只读,但 denom() 会就地修改内部状态(如清零符号位、初始化底层数组),导致数据竞争;并发调用时必须通过同步机制保护。
math/big.rat 的 denom() 和 cmp() 方法并非线程安全——尽管语义上看似只读,但 denom() 会就地修改内部状态(如清零符号位、初始化底层数组),导致数据竞争;并发调用时必须通过同步机制保护。
math/big.Rat 是 Go 标准库中用于高精度有理数运算的重要类型,常被用于金融计算、密码学或需要避免浮点误差的场景。然而,其设计初衷并非为并发安全——*所有以 `Rat为接收者的方法(包括Denom()和Cmp()`)均假设调用方独占该实例**。
关键问题在于:Denom() 并非纯函数。查看源码可确认(Go 1.23 源码):
func (x *Rat) Denom() *Int {
x.b.neg = false // ⚠️ 修改内部 *Int 的符号位
if len(x.b.abs) == 0 {
x.b.abs = x.b.abs.set(natOne) // ⚠️ 延迟初始化底层数组
}
return &x.b
}
这两处写操作(x.b.neg = false 和 x.b.abs 赋值)直接修改 *Rat 的私有字段 b(即分母),而 Cmp() 内部也可能触发类似惰性初始化逻辑(例如对分子/分母的归一化预处理)。因此,*即使多个 goroutine 仅“读取”同一个 `Rat,只要其中任一调用Denom()` 或某些其他方法,就会引发数据竞争**。
✅ 正确做法:使用 sync.RWMutex 实现读写分离
由于 Cmp() 本质是只读(不修改 *Rat 状态),而 Denom() 是写操作(修改内部字段),推荐使用读写锁提升并发性能:
package main
import (
"math/big"
"sync"
)
func main() {
x := big.NewRat(5, 1)
var wg sync.WaitGroup
var mu sync.RWMutex // 保护对 x 的所有访问
wg.Add(10)
for i := 0; i <blockquote>
<p>? <strong>验证竞态</strong>:务必使用 <code>-race</code> 标志运行:</p>
<pre class="brush:php;toolbar:false;">go run -race main.go
原始代码会立即报出 Write at ... by goroutine N / Previous write at ... by goroutine M 等详细竞争栈;加锁后则静默通过。
⚠️ 注意事项与最佳实践
- *切勿共享可变 `Rat
实例**:若需高并发读,应考虑将*Rat视为不可变对象——每次修改(如SetFloat64,Mul,Add)后创建新实例,或使用Rat.Set()` 复制值后再并发读。 -
避免循环变量捕获陷阱:示例中
for i := 0; i 会导致所有 goroutine 共享同一个 <code>i变量(最终值为10)。正确方式是显式传参(如func(i)),已体现在修复代码中。 -
RWMutex不是万能解药:若写操作频繁(如高频调用Denom()),锁争用会成为瓶颈。此时更优策略是提前缓存分母:// 初始化时一次性计算并缓存(假设 Rat 值不再变更) denom := new(big.Int).Set(x.Denom()) // 复制一份不可变副本 // 后续并发读取直接使用 denom,无需锁
? 总结
math/big.Rat 的 Denom() 和 Cmp() 方法不是并发安全的——这不是 bug,而是设计权衡:以空间和性能换灵活性。开发者需主动承担同步责任。*核心原则是:任何对 `Rat的方法调用,只要其接收者是指针且可能修改内部状态,就必须在并发场景下加锁保护。** 推荐优先使用sync.RWMutex` 区分读写,或重构为不可变模式,从根本上规避竞争风险。










