Какие примитивы синхронизации потоков ты знаешь в .NET?

«Какие примитивы синхронизации потоков ты знаешь в .NET?» — вопрос из категории Многопоточность, который задают на 43% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В .NET существует иерархия примитивов синхронизации для управления доступом к общим ресурсам из нескольких потоков. Выбор зависит от сценария и требуемого уровня производительности.

1. Низкоуровневые (на основе атомарных операций)

  • Ключевое слово volatile: Гарантирует, что чтение и запись в поле будут атомарными, а компилятор или процессор не переупорядочат операции с ним. Не является полноценным примитивом синхронизации.
  • Класс Interlocked: Предоставляет атомарные операции для простых действий с числами и ссылками.
    int sharedCounter = 0;
    // Атомарное увеличение
    Interlocked.Increment(ref sharedCounter);
    // Атомарное сравнение и обмен (CAS - Compare-And-Swap)
    int oldValue, newValue;
    do {
        oldValue = sharedCounter;
        newValue = oldValue + 10;
    } while (Interlocked.CompareExchange(ref sharedCounter, newValue, oldValue) != oldValue);

2. Примитивы на основе ядра ОС (медленные, но мощные)

Используют переход в режим ядра, что дорого. Подходят для синхронизации между процессами.

  • Mutex (мьютекс): Примитив взаимоисключения. Может быть именованным для межпроцессного взаимодействия.
  • Semaphore и SemaphoreSlim: Ограничивают количество потоков, которые могут одновременно получить доступ к ресурсу.
  • EventWaitHandle (AutoResetEvent, ManualResetEvent): Позволяют потоку сигнализировать другим потокам о наступлении события.

3. Гибридные и пользовательские примитивы (быстрые для внутрипроцессной синхронизации)**

Сначала пытаются выполнить операцию в пользовательском режиме, при конфликте переходят в режим ядра.

  • lock (ключевое слово) / Monitor: Самый распространенный примитив для обеспечения эксклюзивного доступа к блоку кода.
    private readonly object _syncRoot = new object();
    public void ThreadSafeMethod()
    {
        lock (_syncRoot) // Вход в критическую секцию
        {
            // Работа с общим ресурсом
        }
    }
  • ReaderWriterLockSlim: Оптимизирован для сценариев, где много операций чтения и мало записи. Позволяет множественное чтение, но эксклюзивную запись.
  • ManualResetEventSlim и SemaphoreSlim: Облегченные версии своих аналогов на основе ядра, рекомендуются для использования внутри одного процесса.

Общее правило: Начинайте с самых "легковесных" примитивов (Interlocked, lock). Переходите к более сложным (ReaderWriterLockSlim, SemaphoreSlim) только при доказанной необходимости и понимании их семантики.