Какие способы безопасно заблокировать доступ к общему ресурсу внутри класса в C#?

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

Ответ

Безопасная блокировка (синхронизация) доступа к общему ресурсу из нескольких потоков — ключевая задача. В C# для этого есть несколько механизмов, выбор зависит от контекста.

Основные подходы:

  1. Ключевое слово lock (Monitor.Enter/Exit)

    • Самый распространенный и простой способ для синхронизации доступа к блоку кода внутри одного процесса (AppDomain).
    • Принцип: Использует объект-маркер (object _lock = new object();). Только один поток может владеть этим маркером в данный момент.
    • Важно: Используйте приватный, readonly объект, специально созданный для блокировки. Никогда не используйте this, typeof(...) или публичные объекты — это может привести к взаимоблокировкам (deadlock).

      public class Account
      {
      private readonly object _balanceLock = new object();
      private decimal _balance;
      
      public void Deposit(decimal amount)
      {
          // Только один поток может выполнять этот блок кода для данного экземпляра Account
          lock (_balanceLock)
          {
              _balance += amount;
          }
      }
      }
  2. Класс Mutex (мьютекс)

    • Принцип: Аналогичен lock, но может быть именованным и использоваться для синхронизации между разными процессами (межпроцессная синхронизация).
    • Минусы: Более тяжеловесный, чем lock.
      private static Mutex _mutex = new Mutex(false, "GlobalMyAppMutex");
      public void Process()
      {
      _mutex.WaitOne(); // Захват мьютекса
      try
      {
          // Критическая секция
      }
      finally
      {
          _mutex.ReleaseMutex(); // Освобождение мьютекса ВСЕГДА в finally
      }
      }
  3. Класс Semaphore и SemaphoreSlim

    • Принцип: Позволяет ограничить количество потоков, которые могут одновременно войти в критическую секцию, до заданного числа (больше 1). SemaphoreSlim — легковесная версия для внутрипроцессной синхронизации.
    • Идеально для: Ограничения доступа к пулу ресурсов (например, не более 10 одновременных подключений к внешнему API).
      
      private static SemaphoreSlim _pool = new SemaphoreSlim(initialCount: 3, maxCount: 3);

    public async Task AccessResourceAsync() { await _pool.WaitAsync(); // Ждем, если уже 3 потока внутри try { // Работа с ресурсом (максимум 3 потока одновременно) await Task.Delay(1000); } finally { _pool.Release(); // Освобождаем слот } }

  4. Класс ReaderWriterLockSlim

    • Принцип: Оптимизирован для сценариев, где чтение происходит часто, а запись редко. Позволяет множеству потоков читать данные одновременно, но дает эксклюзивный доступ на запись.
    • Идеально для: Кэшей или конфигураций, которые часто читаются, но изредка обновляются.
      
      private readonly ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim();
      private Dictionary<string, Data> _cache = new();

    public Data GetData(string key) { _cacheLock.EnterReadLock(); // Множественные потоки могут войти сюда try { return _cache[key]; } finally { _cacheLock.ExitReadLock(); } }

    public void UpdateData(string key, Data value) { _cacheLock.EnterWriteLock(); // Эксклюзивная блокировка. Ждет, пока все читатели выйдут. try { _cache[key] = value; } finally { _cacheLock.ExitWriteLock(); } }

Общее правило: Всегда заключайте работу с блокировкой в блок try...finally, чтобы гарантировать ее освобождение даже в случае исключения. Для асинхронных методов предпочитайте SemaphoreSlim.WaitAsync() или другие асинхронные примитивы, чтобы не блокировать потоки пула.