Что такое потокобезопасность (thread safety)?

«Что такое потокобезопасность (thread safety)?» — вопрос из категории Многопоточность, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Потокобезопасность — это свойство кода (класса, метода, структуры данных) корректно работать в многопоточной среде, когда несколько потоков обращаются к нему одновременно, без возникновения ошибок данных или неопределённого поведения.

Основные угрозы, от которых защищает потокобезопасность:

  1. Состояние гонки (Race Condition): Результат выполнения зависит от неуправляемого порядка выполнения потоков.
  2. Некорректные видимые изменения (Visibility): Изменения, сделанные одним потоком в общей памяти, могут быть не сразу (или никогда) видны другому потоку из-за кэширования процессором.
  3. Взаимная блокировка (Deadlock): Два или более потока бесконечно ждут друг друга, освобождая необходимые им ресурсы.

Механизмы обеспечения потокобезопасности в C#:

  • lock (критическая секция): Самый простой способ для защиты блока кода.

    private readonly object _syncLock = new object();
    private int _sharedCounter = 0;
    
    public void IncrementSafe()
    {
        lock (_syncLock) // Только один поток может выполнять этот блок в данный момент.
        {
            _sharedCounter++; // Операция теперь атомарна и безопасна.
        }
    }
  • Атомарные операции с Interlocked: Для простых операций над примитивами (инкремент, сравнение с обменом). Гораздо быстрее lock.

    Interlocked.Increment(ref _sharedCounter);
  • Специализированные примитивы: Monitor, Mutex (для межпроцессного взаимодействия), Semaphore, ReaderWriterLockSlim (оптимизирован для сценариев «много чтений, редкая запись»).

  • Потокобезопасные коллекции (ConcurrentQueue, ConcurrentDictionary): Встроенные реализации, избавляющие от необходимости ручной синхронизации.

  • Неизменяемость (Immutability): Самый надёжный способ. Если объект после создания не может быть изменён, его можно свободно передавать между потоками.

    public record ImmutableData(int Id, string Name); // Record в C# 9+ по умолчанию неизменяем.

Пример опасного (непотокобезопасного) кода:

int counter = 0;
Parallel.For(0, 10000, i =>
{
    counter++; // Операция не атомарна: чтение -> увеличение -> запись.
});
Console.WriteLine(counter); // Результат будет меньше 10000 из-за потерь при гонке.

Важно: Потокобезопасность имеет стоимость (блокировки замедляют код). Не стоит делать потокобезопасным всё подряд. Применяйте синхронизацию только к действительно общим, изменяемым данным, и выбирайте наиболее подходящий и менее затратный механизм для конкретной задачи.