Как решается проблема общего доступа к ресурсам в многопоточной среде?

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

Ответ

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

  1. lock (ключевое слово, обертка над Monitor): Стандартный выбор для синхронизации в рамках одного процесса.

    private readonly object _syncRoot = new object();
    
    public void ModifySharedResource()
    {
        lock (_syncRoot)
        {
            // Критическая секция. Только один поток выполняет этот блок.
            sharedCounter++;
        }
    }
  2. Mutex (мьютекс): Подходит для межпроцессной синхронизации (например, чтобы гарантировать запуск только одного экземпляра приложения).

    using var mutex = new Mutex(false, "Global\MyAppSingletonMutex");
    if (!mutex.WaitOne(TimeSpan.FromSeconds(5)))
    {
        Console.WriteLine("Другой экземпляр приложения уже запущен.");
        return;
    }
    try { /* Работа приложения */ }
    finally { mutex.ReleaseMutex(); }
  3. SemaphoreSlim / Semaphore: Ограничивают количество потоков, которые могут одновременно войти в критическую секцию. SemaphoreSlim легче и рекомендуется для внутрипроцессной работы.

    private readonly SemaphoreSlim _pool = new SemaphoreSlim(2, 2); // Максимум 2 потока
    
    public async Task AccessDatabaseAsync()
    {
        await _pool.WaitAsync();
        try { /* Ограниченный доступ к ресурсу (напр., соединению с БД) */ }
        finally { _pool.Release(); }
    }
  4. ReaderWriterLockSlim: Оптимизирован для сценариев, где чтение происходит часто, а запись редко. Позволяет множественное чтение, но эксклюзивную запись.

    private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
    
    public string ReadData()
    {
        _rwLock.EnterReadLock();
        try { return cachedData; }
        finally { _rwLock.ExitReadLock(); }
    }
    
    public void UpdateData(string newData)
    {
        _rwLock.EnterWriteLock();
        try { cachedData = newData; }
        finally { _rwLock.ExitWriteLock(); }
    }

Общая рекомендация: Для простых случаев используйте lock. Для асинхронного кода — SemaphoreSlim. Для межпроцессного взаимодействия — Mutex. Для оптимизации read-heavy нагрузок — ReaderWriterLockSlim.