带有交换的Golang无锁数组

时间:2018-02-10 17:53:26

标签: go lock-free

我有一个两个插槽阵列,当生产者设置它时需要在插槽之间交换,并且总是向消费者返回一个有效的插槽。至于原子操作逻辑的一面,我无法想象两个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>

1 个答案:

答案 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]

当您想要避免使用互斥锁时,原子性非常好,但在这种情况下不应该用于执行同步。

我不太确定你的原始场景是什么,但是我相信自己会使用互斥锁会为你节省很多痛苦。