Ответ
Без должной синхронизации произойдет состояние гонки (race condition), приводящее к неопределенному поведению, повреждению данных и трудноотлавливаемым багам.
Конкретные проблемы:
- Частичная запись (Tearing): Для полей размером больше машинного слова (например,
double,decimal,DateTime) один поток может читать значение, которое другой поток записывает лишь частично, получая «битый» результат. - Неверные вычисления: Операции инкремента (
i++) не атомарны. Они выполняются как «чтение-изменение-запись». Потоки могут перезаписать результаты друг друга. - Нарушение инвариантов: Если два поля логически связаны (например,
XиYкоординаты), один поток может увидеть новое значениеXи старое значениеY.
Пример состояния гонки:
public class UnsafeCounter
{
public int Count = 0; // Публичное поле — опасно!
}
var counter = new UnsafeCounter();
var tasks = new Task[1000];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = Task.Run(() => counter.Count++); // НЕБЕЗОПАСНО!
}
await Task.WhenAll(tasks);
Console.WriteLine(counter.Count); // Результат будет СЛУЧАЙНЫМ, меньше 1000
Способы синхронизации в C#:
lock(мьютекс): Базовый и самый распространенный способ.private readonly object _syncRoot = new object(); lock (_syncRoot) { counter.Count++; }Interlocked: Для простых атомарных операций (инкремент, сложение, обмен). Самый быстрый.Interlocked.Increment(ref counter.Count);SemaphoreSlim,ReaderWriterLockSlim: Для более сложных сценариев доступа.- Потокобезопасные коллекции (
ConcurrentBag,ConcurrentDictionary): Встроенная синхронизация. - Неизменяемые (immutable) типы: Самый безопасный подход — не использовать общее изменяемое состояние.