Что такое состояние гонки (Race condition)?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Race Condition — конкурентная гонка между потоками
Race condition (состояние гонки) — это ошибка в многопоточной программе, когда результат зависит от порядка выполнения операций несколькими потоками. Потоки конкурируют за доступ к общему ресурсу, и в зависимости от того, какой поток «выиграет гонку», результат будет разным.
Классический пример — увеличение счётчика
var counter = 0 // Общая переменная для двух потоков
fun thread1() {
for (i in 1..1000) {
counter++ // Операция не атомарна!
}
}
fun thread2() {
for (i in 1..1000) {
counter++ // Операция не атомарна!
}
}
// Запуск обоих потоков
thread(block = ::thread1).start()
thread(block = ::thread2).start()
// Ожидаемый результат: 2000
// Фактический результат: может быть любое число от 1000 до 2000!
// Часто получаем: 1234, 1567, 1892 и т.д.
Почему counter++ ненадежна?
Операция counter++ состоит из трёх шагов:
- READ: прочитать текущее значение (например, 5)
- MODIFY: увеличить на 1 (5 + 1 = 6)
- WRITE: записать обратно (записать 6)
Если два потока выполняют это одновременно:
Поток 1: Поток 2: counter
READ (5) → 5
READ (5) 5
MODIFY (6) → 5
MODIFY (6) 5
WRITE (6) → 6
WRITE (6) 6
// Результат: 6 вместо 7 (потеря обновления!)
Реальный пример в Android
Bad — Race condition в UI:
var userBalance = 1000 // Доступно из основного потока и фонового
fun withdrawMoney(amount: Int) {
Thread {
if (userBalance >= amount) { // RACE: баланс может измениться
userBalance -= amount // RACE: может быть negative balance
updateUI("Withdrawn: $amount")
}
}.start()
}
fun deposit(amount: Int) {
userBalance += amount // RACE: конкурирует с withdrawal
}
// Сценарий:
// Баланс: 1000
// 1. Поток withdraw читает баланс: 1000
// 2. Поток deposit меняет баланс: 1000 + 500 = 1500
// 3. Поток withdraw вычитает: 500 (хотя баланс уже обновлен)
// Результат: неправильный баланс!
Решение 1: synchronized
var userBalance = 1000
val lock = Object()
fun withdrawMoney(amount: Int) {
Thread {
synchronized(lock) { // Гарантирует эксклюзивный доступ
if (userBalance >= amount) {
userBalance -= amount
updateUI("Withdrawn: $amount")
}
}
}.start()
}
fun deposit(amount: Int) {
synchronized(lock) {
userBalance += amount
}
}
Решение 2: Atomic классы
import java.util.concurrent.atomic.AtomicInteger
val counter = AtomicInteger(0)
fun thread1() {
for (i in 1..1000) {
counter.incrementAndGet() // Атомарная операция
}
}
fun thread2() {
for (i in 1..1000) {
counter.incrementAndGet() // Атомарная операция
}
}
// Результат: всегда 2000 ✅
Решение 3: Volatile + правильная синхронизация
@Volatile
var userBalance = 1000
fun withdrawMoney(amount: Int) {
synchronized(this) {
if (userBalance >= amount) {
userBalance -= amount
}
}
}
Решение 4: Coroutines + Mutex (Kotlin)
var userBalance = 1000
val balanceLock = Mutex()
suspend fun withdrawMoney(amount: Int) {
balanceLock.withLock { // Гарантирует синхронизацию
if (userBalance >= amount) {
userBalance -= amount
}
}
}
Типичные места Race Conditions в Android
1. SharedPreferences (не thread-safe для write):
// ❌ Может быть race condition
SharedPreferences.edit().putString("key", value).apply()
// ✅ Правильно
SharedPreferences.edit().putString("key", value).commit() // Синхронно
2. Database access:
// ❌ Race condition
val user = getUserFromDb(id)
user.name = "New Name"
saveUserToDb(user)
// ✅ Использовать транзакции
database.withTransaction {
val user = getUserFromDb(id)
user.name = "New Name"
saveUserToDb(user)
}
3. Check-then-act pattern:
// ❌ Race condition между проверкой и действием
if (isLoggedIn) { // RACE: статус может измениться
loadUserData() // RACE: может быть logout
}
// ✅ Синхронизированная проверка и действие
synchronized(lock) {
if (isLoggedIn) {
loadUserData()
}
}
Инструменты для поиска Race Conditions
- ThreadSanitizer (в Java/Kotlin виртуальных машинах)
- Intellij IDEA — анализ потенциальных race conditions
- JUnit tests с множественными потоками
@Test
fun testConcurrentAccess() {
repeat(100) { // Запускаем 100 раз для поиска race conditions
val counter = AtomicInteger(0)
val threads = (1..10).map {
Thread {
repeat(100) {
counter.incrementAndGet()
}
}
}
threads.forEach { it.start() }
threads.forEach { it.join() }
assertEquals(1000, counter.get())
}
}
Правила безопасной многопоточности
- Избегай общего изменяемого состояния (mutable shared state)
- Синхронизируй доступ к общим ресурсам (synchronized, Mutex, AtomicXxx)
- Используй volatile для флагов видимости между потоками
- Предпочитай immutable данные и coroutines вместо raw threads
- Тестируй многопоточный код с большим количеством итераций
Вывод
Race condition — это критическая ошибка, которая возникает, когда несколько потоков конкурируют за доступ к общему ресурсу без синхронизации. Результат выполнения становится недетерминированным. Используй synchronized, volatile, Atomic классы или coroutines с Mutex для защиты критических секций кода.