我有一个两个插槽阵列,当生产者设置它时需要在插槽之间交换,并且总是向消费者返回一个有效的插槽。至于原子操作逻辑的一面,我无法想象两个goroutine写入同一个数组槽的情况,但是竞争检测器不这么认为。有谁能解释我,这个错误在哪里?
type checkConfig struct {
timeout time.Time
}
type checkConfigVersions struct {
config [2]*checkConfig
reader uint32
writer uint32
}
func (c *checkConfigVersions) get() *checkConfig {
return c.config[atomic.LoadUint32(&c.reader)]
}
func (c *checkConfigVersions) set(new *checkConfig) {
for {
reader := atomic.LoadUint32(&c.reader)
writer := atomic.LoadUint32(&c.writer)
switch diff := reader ^ writer; {
case diff == 0:
runtime.Gosched()
case diff == 1:
if atomic.CompareAndSwapUint32(&c.writer, writer, (writer+1)&1) {
c.config[writer] = new
atomic.StoreUint32(&c.reader, writer)
return
}
}
}
}
数据竞赛发生在c.config[writer] = new
,但就我的观点而言,这是不可能的。
fun main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var wg sync.WaitGroup
ccv := &checkConfigVersions{reader: 0, writer: 1}
for i := 0; i < runtime.NumCPU(); i++ {
wg.Add(100)
go func(i int) {
for j := 0; j < 100; j++ {
ccv.set(&checkConfig{})
wg.Done()
}
}(i)
}
wg.Wait()
fmt.Println(ccv.get())
}
数据竞争检测器输出:
==================
WARNING: DATA RACE
Write at 0x00c42009a020 by goroutine 12:
main.(*checkConfigVersions).set()
/Users/apple/Documents/Cyber/Go/proxy/main.go:118 +0xd9
main.main.func1()
/Users/apple/Documents/Cyber/Go/proxy/main.go:42 +0x60
Previous write at 0x00c42009a020 by goroutine 11:
main.(*checkConfigVersions).set()
/Users/apple/Documents/Cyber/Go/proxy/main.go:118 +0xd9
main.main.func1()
/Users/apple/Documents/Cyber/Go/proxy/main.go:42 +0x60
Goroutine 12 (running) created at:
main.main()
/Users/apple/Documents/Cyber/Go/proxy/main.go:40 +0x159
Goroutine 11 (running) created at:
main.main()
/Users/apple/Documents/Cyber/Go/proxy/main.go:40 +0x159
==================
如果你尝试用ccv.read()
读取它,你会抓住另一个种族,但是在同一个阵列插槽上读和写之间... < / p>
答案 0 :(得分:1)
我可以更改您的代码以检查刚编写的c.writer
:
if atomic.CompareAndSwapUint32(&c.writer, writer, (writer+1)&1) {
newWriter := atomic.LoadUint32(&c.writer)
if newWriter != (writer+1)&1 {
panic(fmt.Errorf("wrote %d, but writer is %d", (writer+1)&1, newWriter))
}
//c.config[writer] = new
atomic.StoreUint32(&c.reader, writer)
return
}
将GOMAXPROCS
(以及goroutines的数量)手动设置为3,运行几次后我得到以下内容:
$ go run main.go
panic: wrote 1, but writer is 0
goroutine 6 [running]:
main.(*checkConfigVersions).set(0xc42000a060, 0x1, 0xc4200367a0)
main.go:36 +0x16d
main.main.func1(0xc42000a060, 0xc420018110, 0x1)
main.go:58 +0x63
created by main.main
main.go:56 +0xd6
exit status 2
主要原因是GOMAXPROCS
设置要使用的OS线程数。这些不受Go控制(Go只是将goroutine安排到OS线程上)。相反,操作系统可以根据需要安排操作系统线程。
这意味着其中一个goroutine将CAS(1, 1, 0)
,然后由操作系统暂停。在此期间,另一个goroutine将通过并CAS(0, 0, 1)
。这允许第三个goroutine执行CAS(1, 1, 0)
并且在原始goroutine被安排再次执行的同时继续。巴姆!他们俩都试图写同一个config[writer]
。
当您想要避免使用互斥锁时,原子性非常好,但在这种情况下不应该用于执行同步。
我不太确定你的原始场景是什么,但是我相信自己会使用互斥锁会为你节省很多痛苦。