Детектор гонки 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
4
Jacopo 8 Ноя 2020 в 09:02

2 ответа

Лучший ответ

Эту гонку данных можно решить, сделав lock указателем:

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

Из документов sync.Mutex:

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

Использование типа значения для поля lock приводит к копированию этого мьютекса.

2
Yury Fedorov 8 Ноя 2020 в 08:28

Проблема здесь вовсе не в значении отражения, вы можете воспроизвести гонку на более простом примере:

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 для вывода, то я бы предложил добавить методы String, GoString и / или 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{},
    }
}
2
JimB 9 Ноя 2020 в 13:38