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

Ответ

В 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.

Ответ 18+ 🔞

А, синхронизация потоков в C#! Ну это ж классика, вечная тема, как эти ваши сериалы про врачей. Все думают, что это просто — взял lock и поехал, а потом бац — дедлок, и приложение висит, как будто его кто-то в рот ебал.

Смотри, тут главное — не выебываться с навороченными штуками, если можно проще. Вот прям как в жизни: зачем брать бензопилу, чтобы хлеб нарезать?

1. lock — твой базовый, родной, как тапки домашние. Используешь в 90% случаев, когда нужно внутри одного процесса, чтобы потоки не налетели на общую переменную, как пьяные мухи на стекло. Просто, надёжно, нихуя лишнего.

private readonly object _syncRoot = new object(); // вот этот объект — твой ключ от сортира

public void IncrementCounter()
{
    lock (_syncRoot) // захватил ключ — иди сри один
    {
        sharedValue++; // сделал дело — гуляй смело
    }
}

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 — это как очередь в кабинет к гинекологу. Внутри только N человек одновременно, остальные — ждут на улице и курят. SemaphoreSlim — это лёгкая версия, для асинхронного кода прям ваще красота.

private readonly SemaphoreSlim _pool = new SemaphoreSlim(2, 2); // всего два стула, больше не влезут

public async Task AccessResourceAsync()
{
    await _pool.WaitAsync(); // ждём, пока один из двух выйдет
    try { /* пользуемся ресурсом, типа соединения с базой */ }
    finally { _pool.Release(); } // вышли — следующий заходи
}

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

private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();

public string GetConfig()
{
    _rwLock.EnterReadLock(); // зашли почитать, нас тут много
    try { return _settings; }
    finally { _rwLock.ExitReadLock(); } // ушли, не шумим
}

public void UpdateConfig(string newShit)
{
    _rwLock.EnterWriteLock(); // все, пиздуйте нахуй, я сейчас важное дело делаю
    try { _settings = newShit; }
    finally { _rwLock.ExitWriteLock(); } // готово, можете снова толпиться
}

Итог, ёпта:

  • Обычная хуйня в одном процессе? lock и не парься.
  • Нужно ждать в асинхронном коде? SemaphoreSlim тебе в руки.
  • Хочешь, чтобы приложение в единственном экземпляре было? Mutex, глобальный, как совесть.
  • Чтения дохуя, а записи раз в полгода? ReaderWriterLockSlim, чтобы не стоять в очереди почём зря.

А то возьмёшь не тот примитив — и потом сидишь, дебажишь эти гонки данных, волосы на жопе рвёшь. Не усложняй без нужды, работает и ладно.