Data race это частный случай race condition. Определять термин data race на самом деле можно по-разному.
Самое простое определение:
Состояние во время исполнения, когда две операции над переменной происходят из разных горутин без синхронизации, при этом хотя бы одна из них является записью.
Чуть более сложное:
Состояние во время исполнения, когда две конфликтующие операции над переменной происходят из разных горутин без синхронизации.
И самое сложное на основании формализма из Go memory model:
Состояние во время исполнения, когда две конфликтующие операции над переменной нельзя разделить отношением happens before
Казалось бы, да ну их, есть прекрасный инструмент race detector, который инструментриует программу при компиляции и позволяет во время исполнения находить data race.
Но, внезапно, выясняется, что он не всегда может найти проблему. Это напрямую следует из его устройства.
Пример:
Мейнтейнеры Go пытались реализовать такой интерфейс:
type Cron interface {
Run(ctx context.Context, action func(), next func() time.Duration)
}
Реализация должна вызывать
action, ожидая перед каждым запуском next() времени. После отмены контекста не должно быть вызовов action.Такой подход на практике очень часто используется, например:
• HTTP ретраи
• Service discovery
• Adaptive polling
• Keepalive / heartbeat
• Любые задачи с backoff
Сходу у них получилась следующая реализация:
func (c *cronImpl) Run(ctx context.Context, action func(), next func() time.Duration) {
var t *time.Timer
t = time.AfterFunc(next(), func() {
select {
case <-ctx.Done():
return
default:
action()
t.Reset(next())
}
})
<-ctx.Done()
}
Запускаем тесты:
func TestCron(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
c := New()
var n atomic.Int64
next := func() time.Duration {
if n.Add(1) <= 5 {
return 0
}
return 1 * time.Second
}
var calls atomic.Int64
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(func() {
cancel()
})
done := make(chan struct{})
go func() {
defer close(done)
c.Run(ctx, func() {
calls.Add(1)
}, next)
}()
synctest.Wait()
require.Greater(t, calls.Load(), int64(0))
cancel()
synctest.Wait()
select {
case <-done:
default:
t.Fatalf("Run did not exit after cancel")
}
})
}
Которые успешно проходят даже с race detector'ом.
Но в этой реализации действительно есть data race, и race detector его не находит.
👉👉 Пишите ваши предположения в комментариях, пост с правильным ответом выйдет в ближайшее время.
Ссылка с примером использования: https://go.dev/play/p/Fq-yNa0wXWj
P.S. Пожалуйста, те, кто проходил курс и решал это задание на практике, не пишите сразу правильный ответ)