Ответ
Параллельный доступ нескольких Task к общему изменяемому состоянию без синхронизации приводит к состоянию гонки (race condition). Это вызывает недетерминированное поведение, повреждение данных и трудноуловимые ошибки.
Основные проблемы:
- Потеря обновлений: Два потока читают одно значение, инкрементируют его и записывают обратно. Одно из обновлений теряется.
- Некорректное состояние: Один поток читает данные, пока другой поток находится в процессе их изменения, получая "половинчатое" или неконсистентное состояние объекта.
Наглядный пример:
int sharedCounter = 0;
List<Task> tasks = new();
for (int i = 0; i < 1000; i++)
{
tasks.Add(Task.Run(() => sharedCounter++)); // ОПАСНО: операция не атомарна!
}
await Task.WhenAll(tasks);
Console.WriteLine($"Ожидалось: 1000, Получено: {sharedCounter}");
// Результат будет меньше 1000, например, 987
Способы синхронизации:
| Способ | Применение | Пример для счетчика |
|---|---|---|
Interlocked |
Атомарные операции с примитивами. Самый быстрый. | Interlocked.Increment(ref sharedCounter); |
lock (Monitor) |
Блокировка для защиты произвольного блока кода. Универсально. | lock (_lockObject) { sharedCounter++; } |
Concurrent коллекции |
Потокобезопасные структуры данных. | Использовать ConcurrentBag<T>, ConcurrentDictionary<K,V> |
SemaphoreSlim / Mutex |
Ограничение доступа к ресурсу для N потоков или межпроцессная синхронизация. | Для ограничения пула соединений. |
Вывод: Общее изменяемое состояние требует явной синхронизации. Предпочитайте неизменяемые (immutable) структуры данных и потокобезопасные коллекции, где это возможно.