Что произойдет, если несколько потоков (Thread) обратятся к общим (shared) полям без синхронизации?

Ответ

Без должной синхронизации произойдет состояние гонки (race condition), приводящее к неопределенному поведению, повреждению данных и трудноотлавливаемым багам.

Конкретные проблемы:

  1. Частичная запись (Tearing): Для полей размером больше машинного слова (например, double, decimal, DateTime) один поток может читать значение, которое другой поток записывает лишь частично, получая «битый» результат.
  2. Неверные вычисления: Операции инкремента (i++) не атомарны. Они выполняются как «чтение-изменение-запись». Потоки могут перезаписать результаты друг друга.
  3. Нарушение инвариантов: Если два поля логически связаны (например, 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) типы: Самый безопасный подход — не использовать общее изменяемое состояние.

Ответ 18+ 🔞

Да ты посмотри на эту дичь, блядь! Без синхронизации у тебя начнётся состояние гонки, понимаешь? Это как два мудака пытаются одним шлангом пожарным в одну дырку залить — в итоге оба обоссутся, а дом сгорит. Поведение будет неопределённым, данные поедят, а баги будут такие, что искать их — как иголку в стоге сена, который ещё и горит, блядь.

Конкретно, что сломается:

  1. Частичная запись: Если поле большое (типа double там), один поток начнёт его писать, а второй в этот момент прочитает — и получит какую-то ересь, наполовину старое, наполовину новое значение. Короче, билиберда полная.
  2. Кривые вычисления: Вот эта операция i++ — она ж не атомарная! Она «прочитай-измени-запиши» делается. И если два потока в неё влетают одновременно, они друг другу результаты перетрут. Вместо двух прибавлений будет одно, блядь.
  3. Нарушение логики: Допустим, у тебя координаты 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. Красота!

Чем лечить, варианты:

  • lock (мьютекс): Классика, как дверной замок. Зашёл — сделал дело — вышел.
    private readonly object _syncRoot = new object();
    lock (_syncRoot) { counter.Count++; }
  • Interlocked: Для простых операций — инкремент, сложение. Быстрее всего, атомарно на уровне процессора.
    Interlocked.Increment(ref counter.Count);
  • SemaphoreSlim, ReaderWriterLockSlim: Когда нужно похитрее, например, много читателей, но один писатель.
  • Потокобезопасные коллекции (ConcurrentBag и прочие): Там уже всё за тебя порешено, бери и пользуйся.
  • Неизменяемые типы: А самый крутой способ — это вообще не делиться изменяемым состоянием. Сделал новую версию объекта — и отдал её. Никаких гонок, потому что меняться нечему. Идеально, блядь.