使用互斥锁和反射时的竞争条件

Race condition when using mutex and reflect

Go 竞态检测器在使用互斥锁时报告竞态条件并反映在被锁定的结构上,示例代码如下。即使反射和对结构成员的访问都通过锁定互斥锁来保护,竞争检测器仍然报告竞争条件。

如何解决争用?

代码:

// main file
package main

import (
    "fmt"
    "reflect"
    "sync"
)

type TestType struct {
    counter uint64
    lock sync.Mutex
}

func NewTestType() *TestType {
    t := &TestType{
        counter: 0,
        lock:    sync.Mutex{},
    }

    go func() {
        t.lock.Lock()
        defer t.lock.Unlock()
        t.counter++
    }()

    return t
}

func ItShouldNotRace() string {
    t := NewTestType()

    t.lock.Lock()
    defer t.lock.Unlock()

    val := reflect.ValueOf(t)
    iface := val.Interface()
    return fmt.Sprintf("%v", iface)
}

// test file
package main

import (
    "testing"
)

func TestItShouldNotRace(t *testing.T) {
    if ItShouldNotRace() == "impossible" {
        t.Fail()
    }
}

竞争检测器输出:

==================
WARNING: DATA RACE
Read at 0x00c000120078 by goroutine 7:
  reflect.typedmemmove()
      /usr/local/opt/go/libexec/src/runtime/mbarrier.go:177 +0x0
  reflect.packEface()
      /usr/local/opt/go/libexec/src/reflect/value.go:120 +0x12f
  reflect.valueInterface()
      /usr/local/opt/go/libexec/src/reflect/value.go:1045 +0x1cd
  reflect.Value.Interface()
      /usr/local/opt/go/libexec/src/reflect/value.go:1015 +0x3aa4
  fmt.(*pp).printValue()
      /usr/local/opt/go/libexec/src/fmt/print.go:726 +0x3aa5
  fmt.(*pp).printValue()
      /usr/local/opt/go/libexec/src/fmt/print.go:880 +0x25fc
  fmt.(*pp).printArg()
      /usr/local/opt/go/libexec/src/fmt/print.go:716 +0x26b
  fmt.(*pp).doPrintf()
      /usr/local/opt/go/libexec/src/fmt/print.go:1030 +0x326
  fmt.Sprintf()
      /usr/local/opt/go/libexec/src/fmt/print.go:219 +0x73
  go_issue.ItShouldNotRace()
      /Users/dedalus/Downloads/go_issue/main.go:37 +0x1a4
  go_issue.TestItShouldNotRace()
      /Users/dedalus/Downloads/go_issue/main_test.go:8 +0x2f
  testing.tRunner()
      /usr/local/opt/go/libexec/src/testing/testing.go:1127 +0x202

Previous write at 0x00c000120078 by goroutine 8:
  sync/atomic.CompareAndSwapInt32()
      /usr/local/opt/go/libexec/src/runtime/race_amd64.s:293 +0xb
  sync.(*Mutex).lockSlow()
      /usr/local/opt/go/libexec/src/sync/mutex.go:129 +0x14b
  sync.(*Mutex).Lock()
      /usr/local/opt/go/libexec/src/sync/mutex.go:81 +0x84
  go_issue.NewTestType.func1()
      /Users/dedalus/Downloads/go_issue/main.go:21 +0x47

Goroutine 7 (running) created at:
  testing.(*T).Run()
      /usr/local/opt/go/libexec/src/testing/testing.go:1178 +0x796
  testing.runTests.func1()
      /usr/local/opt/go/libexec/src/testing/testing.go:1449 +0xa6
  testing.tRunner()
      /usr/local/opt/go/libexec/src/testing/testing.go:1127 +0x202
  testing.runTests()
      /usr/local/opt/go/libexec/src/testing/testing.go:1447 +0x5aa
  testing.(*M).Run()
      /usr/local/opt/go/libexec/src/testing/testing.go:1357 +0x4eb
  main.main()
      _testmain.go:43 +0x236

Goroutine 8 (running) created at:
  go_issue.NewTestType()
      /Users/dedalus/Downloads/go_issue/main.go:20 +0x7a
  go_issue.ItShouldNotRace()
      /Users/dedalus/Downloads/go_issue/main.go:30 +0x54
  go_issue.TestItShouldNotRace()
      /Users/dedalus/Downloads/go_issue/main_test.go:8 +0x2f
  testing.tRunner()
      /usr/local/opt/go/libexec/src/testing/testing.go:1127 +0x202
==================
--- FAIL: TestItShouldNotRace (0.00s)
    testing.go:1042: race detected during execution of test

这个数据竞争可以通过使 lock 成为一个指针来解决:

type TestType struct {
    counter uint64
    lock *sync.Mutex
}

来自 sync.Mutex 文档:

// A Mutex must not be copied after first use.

lock 字段使用值类型会导致复制此互斥量。

这里的问题根本不是反射值,你可以用一个更简单的例子来复制比赛:

type TestType struct {
    sync.Mutex
}

func main() {
    t := &TestType{}

    go func() {
        t.Lock()
        defer t.Unlock()
    }()

    t.Lock()
    defer t.Unlock()

    fmt.Printf("%v\n", t)
}

问题是 fmt.Printf 调用将遍历提供的值以格式化输出,并且在该过程中它必须读取 sync.Mutex 值本身。这意味着竞争是在读取互斥量值(互斥量不应该被“读取”,因为它不是可以复制的值。fmt 确实 读取并打印一个私有互斥锁值是一个有争议的错误,但目前还不能真正改变)

如果这是您打算经常传递给 fmt 以用于输出的值,那么我建议添加 StringGoString and/or Format 方法在不读取互斥体本身的情况下创建字符串值。

在上面的示例中简单地添加 fmt.Stringer 就可以避免竞争条件。

func (t *TestType) String() string {
    return "TestType{}"
}

如果您需要锁定值以便同时读取其他字段,这也很方便。如果我们在您的 counter 字段中添加回来,我们需要序列化该访问(注意您需要删除外部锁定调用以防止此处出现死锁):

func (t *TestType) String() string {
    t.Lock()
    defer t.Unlock()
    return fmt.Sprintf("TestType{counter:%d}", t.counter)
}

避免这种情况的另一种方法是使用互斥锁的指针,但是这会阻止您使用倾向于首选的零值,并且您仍然需要确保在外部锁定该值,以便另一个可以安全地读取字段。由于您已经有了一个构造函数,这可能不是问题,但这是需要考虑的事情

type TestType struct {
    *sync.Mutex
}

func NewTestType() *TestType {
    return &TestType{
        &sync.Mutex{},
    }
}