Sitemap

Open in app

Write

German Gorelkin

·
Follow publication

Research and Development

Follow publication

Race condition и Data Race

German Gorelkin

Follow

5 min read
·
Jun 10, 2019

44

Share

More

Продолжаем серию статей о проблемах многопоточности, параллелизме, concurrency и других интересных штуках.

Race condition и Data Race

Deadlocks, Livelocks и Starvation

Примитивы синхронизации в Go

Безопасная работа с каналами в Go

Goroutine Leaks

Zoom image will be displayed

by Defog Tech

Race Condition и Data Race

Начинаем серию статей о проблемах многопоточности, параллелизме, concurrency и других интересных штуках. Race condition…

ubiklab.net

Race condition и data race — две разные проблемы многопоточности, которые часто путают. Попробуем разобраться.

Race condition

Существует много формулировок определения:

Race condition представляет собой класс проблем, в которых корректное поведение системы зависит от двух независимых событий, происходящих в правильном порядке, однако отсутствует механизм, для того чтобы гарантировать фактическое возникновение этих событий.

Race condition — ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода.

Race condition — это нежелательная ситуация, которая возникает, когда устройство или система пытается выполнить две или более операций одновременно, но из-за природы устройства или системы, операции должны выполняться в правильной последовательности, чтобы быть выполненными правильно.

Race condition — это недостаток, связанный с синхронизацией или упорядочением событий, что приводит к ошибочному поведению программы.

Но мне нравиться наиболее короткое и простое:

Race condition — это недостаток, возникающий, когда время или порядок событий влияют на правильность программы.

Важно, что Race condition — это семантическая ошибка.

В проектирование электронных схем есть похожая проблема:

Состязание сигналов — явление в цифровых устройствах несоответствия работы данного устройства с заданным алгоритмом работы по причине возникновения переходных процессов в реальной аппаратуре.

Рассмотрим пример, где результат не определен:

go func() {
fmt.Printf("A->")
}()

go func() {
fmt.Printf("B")
}()

Если запустить такой код много раз, то можно увидеть примерно такое:

A->B
A->B
A->B
A->B
BA->
A->B

Результат выполнения кода зависит от порядка выполнения горутин. Это типичная ошибка race condition. Ситуации могут быть гораздо сложней и не очевидней.

Учитывая, что race condition семантическая ошибка, нет общего способа который может отличить правильное и неправильное поведение программы в общем случае.

Помочь могут хорошие практики и проверенные паттерны.

Еще один пример:

x := 0
for { go func() {
x++
}() go func() {
if x%2 == 0 {
time.Sleep(1 * time.Millisecond)
fmt.Println(x)
}
}()
}

В результате на консоле получим четные и нечетные числа, а расчитывали увидеть только четные.

Проблемы с доступом к общим ресурсам проще обнаружить автоматически и решаются они обычно с помощью синхронизации:

var mu sync.Mutex
x := 0
for{
go func() {
mu.Lock()
x++
mu.Unlock()
}()
go func() {
mu.Lock()
if x%2 == 0 {
time.Sleep(1 * time.Millisecond)
fmt.Println(x)
}
mu.Unlock()
}()
}

или локальной копией:

x := 0
for i := 0; i < 1000; i++ {
go func() {
x++
}()
go func() {
y := x
if y%2 == 0 {
time.Sleep(1 * time.Millisecond)
fmt.Println(y)
}
}()
}

Data Race

Data race это состояние когда разные потоки обращаются к одной ячейке памяти без какой-либо синхронизации и как минимум один из потоков осуществляет запись.

Пример с балансом на счету:

type account struct {
balance int
}
func deposit(acc *account, amount int) {
acc.balance += amount
}

Запускаем в разных горутинах:

acc := account{balance: 0}
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
wg.Add(1)
go func(n int) {
deposit(&acc, 1)
wg.Done()
}(i)
}
wg.Wait()

fmt.Printf("balance=%d\n", acc.balance)

Изначально баланс равен 0, депозитим 1000 раз по 1. Ожидаем баланс равный 1000, но результат другой:

balance=876

Потеряли много денег.

Причина в том, что операция acc.balance += amount не атомарная. Она может разложиться на 3:

tmp := acc.balance
tmp = tmp + amount
acc.balance = tmp

Пока мы меняем временную переменную в одном потоке, в других уже изменен основной balance. Таким образом теряется часть изменений.

Например, у нас 2 параллельных потока выполнения, каждый должен прибавить к балансу по 1:

tmp := acc.balance // 100 || tmp := acc.balance // 100
tmp = tmp + amount // 101 || tmp = tmp + amount // 101
acc.balance = tmp // 101 || acc.balance = tmp // 101

Ожидали получить баланс=102, а получили = 101.

У Data Race есть точное определение, которое не обязательно связано с корректностью, и поэтому их можно обнаружить. Существует множество разновидностей детекторов гонки данных (статическое/динамическое обнаружение, обнаружение на основе блокировок, обнаружение основанное на предшествующих событий, обнаружение гибридного data race).

У Go есть хороший Data Race Detector с помощью которого такие ошибки можно обнаружить.

Решается проблема с помощью синхронизации:

var mu sync.Mutex

func deposit(acc *account, amount int) {
mu.Lock()
acc.balance += amount
mu.Unlock()
}

Иногда более эффективным решением будет использовать пакет atomic.

func deposit(acc *account, amount int64) {
atomic.AddInt64(&acc.balance, amount)
}

Race Condition и Data Race

Функция для перевода средств с одного счета на другой:

func transfer1(accFrom, accTo *account, amount int) error {
if accFrom.balance < amount {
return fmt.Errorf("accFrom.balance<amount")
}
accTo.balance += amount
accFrom.balance -= amount
return nil
}

На одном счету у нас будет 1000, а на другом 0. Переводим по 1 в 1000 горутинах и ожидаем, что все деньги из одного счета перетекут в другой:

accFrom := account{balance: 1000}
accTo := account{balance: 0}
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
wg.Add(1)
go func(n int) {
err := transfer1(&accFrom, &accTo, 1)
if err != nil {
fmt.Printf("error for n=%d\n", n)
}
wg.Done()
}(i)
}
wg.Wait()

fmt.Printf("accFrom.balance=%d\naccTo.balance=%d\n", accFrom.balance, accTo.balance)

Но результат может быть таким:

accFrom.balance=84
accTo.balance=915

Если запустить цикл на большее кол-во операций, то можно получить еще интересней:

accFrom.balance=0
accTo.balance=997

При вызове из нескольких потоков без внешней синхронизации эта функция допускает как dara race (несколько потоков могут одновременно пытаться обновить баланс счета), так и race condition (в параллельном контексте это приведет к потере денег).

Для решения можно применить синхронизацию и локальную копию. Общая логика может быть не такой линейной и в итоге код может выглядит например так:

func transfer2(accFrom, accTo *account, amount int) error {
mu.Lock()
bal := accFrom.balance
mu.Unlock()

if bal < amount {
return fmt.Errorf("accFrom.balance<amount")
} mu.Lock()
accTo.balance += amount
mu.Unlock()

mu.Lock()
accFrom.balance -= amount
mu.Unlock()

return nil
}

У нас синхронизированы все участки с записью и чтением, у нас есть локальная копия, Race Detector больше не ругается на код. Запускаем 1000 операций и получаем верный результат:

accFrom.balance=0
accTo.balance=1000

Но что если горутин будет 10к:

accFrom.balance=-15
accTo.balance=1015

Мы решили проблему data race, но race condition остался. В данном случае можно сделать блокировку на всю логику перевода средств, но это не всегда возможно.

Решив Data Race через синхронизацию доступа к памяти (блокировки) не всегда решается race condition и logical correctness.

Код примеров github.

На сегодня все. Спасибо!

Дополнительная информация

https://blog.regehr.org/archives/490

https://dzone.com/articles/race-condition-vs-data-race

https://golang.org/doc/articles/race_detector.html

https://en.wikipedia.org/wiki/Race_condition

Golang

Go

Software Development

Coding

Programming

44

44

Follow

Published in German Gorelkin

112 followers

·Last published Oct 21, 2023

Research and Development

Follow

Follow

Written by German Gorelkin

301 followers

·11 following

https://ubiklab.net/

Follow

No responses yet

Kosha's thoughts

What are your thoughts?

Cancel
Respond

More from German Gorelkin and German Gorelkin

In

German Gorelkin

by

German Gorelkin

Low Coupling и High Cohesion

Качественный дизайн обладает слабой связанностью (low coupling) и сильной связностью (high cohesion). Это значит, что программный…

Sep 24, 2019

428

4

In

German Gorelkin

by

German Gorelkin

Deadlocks, Livelocks и Starvation

Рассмотрим проблемы синхронизации в контексте современных языков программирования.

Jan 27, 2020

41

2

In

German Gorelkin

by

German Gorelkin

Embedding в Go

Go предлагает механизм встраивания(embedding) в качестве альтернативы механизму наследования в традиционных объектно-ориентированных…

Oct 4, 2018

34

1

In

German Gorelkin

by

German Gorelkin

Примитивы синхронизации в Go

Пакет sync содержит примитивы, которые наиболее полезны для низкоуровневой синхронизации доступа к памяти. WaitGroup. Mutex. RWMutex. Cond.

Feb 9, 2020

58

1

See all from German Gorelkin

See all from German Gorelkin

Recommended from Medium

Abhinav

Go Just Fixed Its Biggest Flaw — Quietly

If you’ve written real-world code in Go, you already know the pain.

Jul 24

302

6

Ömer Burak Demirpolat

Go Interfaces: When to Use Them and When to Skip Them

In this article, I will attempt to briefly answer the question of when to use an interface.

Jul 21

10

In

Stackademic

by

Kavya's Programming Path

I Finally Understood Multithreading — And Now My Code Runs 10x Faster

“Multithreading sounds complex until you understand it. Then it becomes one of your most powerful tools.” — Every Senior Developer Ever

Jul 24

332

5

Moksh S

GOMAXPROCS vs Containers: What Changed in Go 1.25?

When running Go applications in containers, developers often assume their code only uses the CPU limit they set — for example, --cpus=1…

Jul 25

4

Thread Whisperer

Go Concurrency: 10 Interview Questions That’ll Trip You Up (Unless You’ve Mastered This)

You Think You Know Go Routines? Wait Until the Real Interview.

Jul 17

4

Quantum Anomaly

Decoding Redis

Redis (REmote DIctionary Server) is an open-source, in-memory, key-value data store, often used as a cache, message broker, real-time…

Jun 13

4

1

See more recommendations

Help

Status

About

Careers

Press

Blog

Privacy

Rules

Terms

Text to speech