Исправление Race Condition в многопоточном коде
Условие
В следующем коде есть race condition. Найдите и исправьте проблему.
public class BankAccount { private decimal _balance;
public BankAccount(decimal initialBalance)
{
_balance = initialBalance;
}
public decimal Balance => _balance;
public bool Withdraw(decimal amount)
{
if (_balance >= amount)
{
Thread.Sleep(1); // Simulates processing time
_balance -= amount;
return true;
}
return false;
}
public void Deposit(decimal amount)
{
Thread.Sleep(1); // Simulates processing time
_balance += amount;
}
}
// Usage causing race condition: var account = new BankAccount(100); var tasks = new List<Task>(); for (int i = 0; i < 10; i++) { tasks.Add(Task.Run(() => account.Withdraw(20))); } Task.WaitAll(tasks.ToArray()); // Expected: 0, Actual: may be negative!
Задание:
- Объясните, почему возникает race condition
- Исправьте код, используя подходящий механизм синхронизации
- Предложите альтернативные решения (lock, Interlocked, SemaphoreSlim)
Критерии оценки:
- Понимание проблемы
- Выбор правильного механизма синхронизации
- Избежание deadlock
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Race Condition в многопоточном коде
Почему возникает race condition?
Проблема состояния в следующем:
- Проверка не атомарна — Код проверяет
if (_balance >= amount), но в момент после проверки другой поток может снять деньги - Несинхронизированный доступ — Два потока одновременно могут читать одно значение _balance
- Временное окно —
Thread.Sleep(1)создаёт большое окно для race condition
Пример сценария:
Поток 1: if (100 >= 20) ✓ проверка пройдена
Поток 2: if (100 >= 20) ✓ проверка пройдена
Поток 1: _balance -= 20 → _balance = 80
Поток 2: _balance -= 20 → _balance = 60 (должно быть 80!)
Результат: Снято 40, хотя проверка прошла для каждого отдельно. После 6 операций вывода 120 баланс может быть отрицательным.
Решение 1: lock (Monitor) — самое простое
public class BankAccount
{
private decimal _balance;
private readonly object _lockObject = new object();
public BankAccount(decimal initialBalance)
{
_balance = initialBalance;
}
public decimal Balance
{
get
{
lock (_lockObject)
{
return _balance;
}
}
}
public bool Withdraw(decimal amount)
{
lock (_lockObject) // Блокируем весь метод
{
if (_balance >= amount)
{
Thread.Sleep(1); // Теперь это безопасно
_balance -= amount;
return true;
}
return false;
}
}
public void Deposit(decimal amount)
{
lock (_lockObject)
{
Thread.Sleep(1);
_balance += amount;
}
}
}
Преимущества:
- Простая реализация
- Надёжно защищает критическую секцию
- Разработана за 30+ лет, работает везде
Недостатки:
- Может быть медленно при высокой конкуренции
- Нет timeout
- Сложнее отладить deadlock
- Всем потокам нужно ждать одного lock
Решение 2: ReaderWriterLockSlim — оптимизация для чтения
public class BankAccount
{
private decimal _balance;
private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
public BankAccount(decimal initialBalance)
{
_balance = initialBalance;
}
public decimal Balance
{
get
{
_rwLock.EnterReadLock();
try
{
return _balance;
}
finally
{
_rwLock.ExitReadLock();
}
}
}
public bool Withdraw(decimal amount)
{
_rwLock.EnterWriteLock();
try
{
if (_balance >= amount)
{
Thread.Sleep(1);
_balance -= amount;
return true;
}
return false;
}
finally
{
_rwLock.ExitWriteLock();
}
}
public void Deposit(decimal amount)
{
_rwLock.EnterWriteLock();
try
{
Thread.Sleep(1);
_balance += amount;
}
finally
{
_rwLock.ExitWriteLock();
}
}
}
Преимущества:
- Множество читателей могут работать одновременно
- Только писатели блокируют друг друга
Недостатки:
- Сложнее в реализации
- Медленнее lock при низкой конкуренции
- Нужны try/finally блоки
Решение 3: SemaphoreSlim — с timeout
public class BankAccount
{
private decimal _balance;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
public BankAccount(decimal initialBalance)
{
_balance = initialBalance;
}
public decimal Balance
{
get
{
_semaphore.Wait();
try
{
return _balance;
}
finally
{
_semaphore.Release();
}
}
}
public bool Withdraw(decimal amount)
{
// Ждём с timeout 5 секунд
if (!_semaphore.Wait(TimeSpan.FromSeconds(5)))
throw new OperationCanceledException("Не удалось получить доступ");
try
{
if (_balance >= amount)
{
Thread.Sleep(1);
_balance -= amount;
return true;
}
return false;
}
finally
{
_semaphore.Release();
}
}
public async Task<bool> WithdrawAsync(decimal amount)
{
await _semaphore.WaitAsync(TimeSpan.FromSeconds(5));
try
{
if (_balance >= amount)
{
_balance -= amount;
return true;
}
return false;
}
finally
{
_semaphore.Release();
}
}
}
Преимущества:
- Поддержка timeout и CancellationToken
- Async/await поддержка
- Избегает deadlock благодаря timeout
Недостатки:
- Нужно обрабатывать OperationCanceledException
- Чуть медленнее lock
Решение 4: Interlocked операции — для простых типов
public class BankAccount
{
private long _balanceCents; // Хранить в центах (long)
public BankAccount(decimal initialBalance)
{
_balanceCents = (long)(initialBalance * 100);
}
public decimal Balance => _balanceCents / 100m;
public bool Withdraw(decimal amount)
{
long amountCents = (long)(amount * 100);
long originalBalance, newBalance;
do
{
originalBalance = Interlocked.Read(ref _balanceCents);
if (originalBalance < amountCents)
return false; // Недостаточно средств
newBalance = originalBalance - amountCents;
}
while (Interlocked.CompareExchange(ref _balanceCents, newBalance, originalBalance) != originalBalance);
return true;
}
public void Deposit(decimal amount)
{
long amountCents = (long)(amount * 100);
long originalBalance, newBalance;
do
{
originalBalance = Interlocked.Read(ref _balanceCents);
newBalance = originalBalance + amountCents;
}
while (Interlocked.CompareExchange(ref _balanceCents, newBalance, originalBalance) != originalBalance);
}
}
Преимущества:
- Самый быстрый вариант (lock-free)
- Не требует блокировок
- Хорошо для высоконагруженных систем
Недостатки:
- Сложная реализация
- Работает только с простыми типами (long, int)
- Нужно понимать CAS (Compare-And-Swap)
Решение 5: Immutable Design Pattern
public record BankAccountState(decimal Balance);
public class BankAccount
{
private BankAccountState _state;
private readonly object _lockObject = new object();
public BankAccount(decimal initialBalance)
{
_state = new BankAccountState(initialBalance);
}
public decimal Balance
{
get
{
lock (_lockObject)
{
return _state.Balance;
}
}
}
public bool Withdraw(decimal amount)
{
lock (_lockObject)
{
if (_state.Balance >= amount)
{
Thread.Sleep(1);
_state = _state with { Balance = _state.Balance - amount };
return true;
}
return false;
}
}
public void Deposit(decimal amount)
{
lock (_lockObject)
{
Thread.Sleep(1);
_state = _state with { Balance = _state.Balance + amount };
}
}
}
Преимущества:
- Все изменения очевидны
- Сложнее сделать ошибку
- Функциональный подход
Недостатки:
- Чуть медленнее из-за создания новых объектов
Тестирование на race condition
[Test]
public void Withdraw_ConcurrentAccess_BalanceNeverNegative()
{
var account = new BankAccount(100);
var tasks = new List<Task>();
// 10 потоков снимают по 20
for (int i = 0; i < 10; i++)
{
tasks.Add(Task.Run(() =>
{
bool result = account.Withdraw(20);
Assert.That(result, Is.True);
}));
}
Task.WaitAll(tasks.ToArray());
// Баланс не может быть отрицательным
Assert.That(account.Balance, Is.GreaterThanOrEqualTo(0));
// И не может быть выше 100
Assert.That(account.Balance, Is.LessThanOrEqualTo(100));
}
[Test]
public void Withdraw_MixedOperations_ConsistentBalance()
{
var account = new BankAccount(1000);
var random = new Random(42);
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
tasks.Add(Task.Run(() =>
{
if (random.Next(2) == 0)
account.Deposit(10);
else
account.Withdraw(5);
}));
}
Task.WaitAll(tasks.ToArray());
// Баланс должен быть позитивный
Assert.That(account.Balance, Is.GreaterThan(0));
}
Рекомендации по выбору
lock — Используй в 95% случаев:
- Простая реализация
- Хорошая производительность
- Легко отладить
SemaphoreSlim — Когда нужен timeout или async:
- API требует async
- Нужна защита от deadlock
- Timeout важен
ReaderWriterLockSlim — Много читателей, мало писателей:
- Профилирование показало узкое место на lock
- Операции чтения намного чаще писания
Interlocked — Высоконагруженные системы:
- Критическая производительность
- Опытные разработчики
- Простой тип данных