Комментарии 4
В коде примера метод
Hello
объявлен на ресивере указателяBen
илиJerry
. Если вместо этого объявить метод на значенииBen
илиJerry
, решит ли это проблему гонки данных?
Не решит, т.к. во первых проблема не в данных интерфейса, а в том, что тип и данные записываются разными операциями записи, между которыми может вклинится другая операция. И в самом интерфейсе всегда хранится ссылка на данные (uintptr). Не важно, что мы определили приемником значение, а не ссылку.
Или я не прав?
Это только полбеды. Здесь гонка сразу в трех местах: при записи в loop0
и loop1
и при чтении в цикле. Даже если мы синхронизируем изменение maker
мы все равно будем иметь туже самую проблему.
Упрощенно этот пример можно было бы переписать следующим образом:
package main
import (
"fmt"
)
type Ben struct {
name string
}
func BenHello(b *Ben) {
fmt.Printf("Ben says, \"Hello my name is %s\"\n", b.name)
}
type Jerry struct {
name string
}
func JerryHello(j *Jerry) {
fmt.Printf("Jerry says, \"Hello my name is %s\"\n", j.name)
}
func main() {
var ben = &Ben{"Ben"}
var jerry = &Jerry{"Jerry"}
var maker interface{} = ben
var loop0, loop1 func()
loop0 = func() {
maker = ben
go loop1()
}
loop1 = func() {
maker = jerry
go loop0()
}
go loop0()
for {
switch m := maker.(type) { // <--- очевидно, что эта операция
case *Jerry:
JerryHello(m) // <--- и эта
case *Ben:
BenHello(m) // <--- не атомарны
}
}
}
Между проверкой типа maker
и вызовом JerryHello
/BenHello
значение maker
может поменяться, даже если мы защитим loop0/loop1 мьютексом. Очевидно, если мы заменим указатель значением, то ничего не поменяется и switch никуда не уйдет. Полагаю, что примерно тоже самое происходит и с интерфейсом.
Впрочем это легко проверить.
Синхронизируем только запись: https://go.dev/play/p/KJrxnbf6pB9
func main() {
...
var lock sync.Mutex
...
loop0 = func() {
lock.Lock()
defer lock.Unlock()
maker = ben
go loop1()
}
loop1 = func() {
lock.Lock()
defer lock.Unlock()
maker = jerry
go loop0()
}
...
}
Результат:
panic: I'm Ben! Not Jerry
goroutine 1 [running]:
main.(*Ben).Hello(0xc00003e730?)
/tmp/sandbox17993843/prog.go:18 +0x59
main.main()
/tmp/sandbox17993843/prog.go:61 +0x1cc
Не помогло. А если синхронизировать только чтение? https://go.dev/play/p/DIcbbDLZPVL
func main() {
...
var lock sync.Mutex
...
for {
lock.Lock()
maker.Hello()
lock.Unlock()
}
}
Тоже не помогло.
Последняя попытка, объединяем оба кода: https://go.dev/play/p/PLucwu_K25t
Вот только теперь работает как надо.
Кстати, в данном случае можно было бы без мьютекса обойтись.
func main() {
var ben = &Ben{"Ben"}
var jerry = &Jerry{"Jerry"}
var maker IceCreamMaker = ben
ch := make(chan IceCreamMaker, 1)
defer close(ch)
var loop0, loop1 func()
loop0 = func() {
ch <- ben
go loop1()
}
loop1 = func() {
ch <- jerry
go loop0()
}
go loop0()
for {
select {
case m := <-ch:
maker = m
default:
maker.Hello()
}
}
}
А почему -race не детектит гонку данных для примера кода выше?
Практика Go — Concurrency