Каким образом решали проблему синхронизации потоков в C#?

Ответ

Выбор механизма синхронизации зависит от конкретной задачи. Вот основные подходы, которые я применяю на практике:

1. lock (ключевое слово для Monitor) Используется для простых сценариев синхронизации внутри одного процесса.

private readonly object _syncRoot = new object();

lock (_syncRoot)
{
    // Критическая секция. Например, изменение общего списка.
    _sharedList.Add(item);
}

Почему lock? Это самый простой и производительный способ для большинства случаев. Важно использовать выделенный приватный объект (_syncRoot), а не this или тип, чтобы избежать внешних deadlock.

2. SemaphoreSlim Идеален для ограничения количества потоков, одновременно получающих доступ к ресурсу, особенно в асинхронном коде.

private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5); // Макс 5 потоков

await _semaphore.WaitAsync();
try
{
    await AccessLimitedResourceAsync();
}
finally
{
    _semaphore.Release();
}

Почему SemaphoreSlim? Он легче, чем классический Semaphore, и имеет асинхронный API (WaitAsync), что предотвращает блокировку потоков пула.

3. ReaderWriterLockSlim Применяется для оптимизации сценариев «много читателей, редко писатель».

private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();

// Чтение (множественный доступ)
_rwLock.EnterReadLock();
try
{
    return _cache.Get(key);
}
finally
{
    _rwLock.ExitReadLock();
}

// Запись (эксклюзивный доступ)
_rwLock.EnterWriteLock();
try
{
    _cache.Set(key, value);
}
finally
{
    _rwLock.ExitWriteLock();
}

Почему ReaderWriterLockSlim? Он значительно повышает производительность, когда операции чтения преобладают над операциями записи.

4. Mutex Нужен для координации между процессами (например, чтобы гарантировать запуск только одного экземпляра приложения).

bool createdNew;
using var mutex = new Mutex(true, "Global\MyAppMutex", out createdNew);
if (!createdNew)
{
    // Другой экземпляр приложения уже запущен.
    return;
}
// Продолжаем работу...

Ключевые принципы, которых я придерживаюсь:

  • Всегда освобождайте блокировки в finally для гарантии избежания deadlock.
  • Минимизируйте время удержания блокировки — выполняйте внутри критической секции только необходимые операции.
  • Избегайте вложенных блокировок или строго соблюдайте единый порядок их захвата.
  • Для асинхронного кода предпочитайте SemaphoreSlim, async/await и конкурентные коллекции (ConcurrentBag, ConcurrentDictionary) вместо ручной синхронизации там, где это возможно.

Ответ 18+ 🔞

А, синхронизация, говоришь? Ну это ж, блядь, святое дело, если хочешь чтобы твоё приложение не разъехалось по швам как пьяный дед на льду. Слушай сюда, я тебе сейчас по полочкам разложу, как я с этим добром обращаюсь.

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

private readonly object _syncRoot = new object(); // Вот этот самый чурбачок

lock (_syncRoot)
{
    // Тут твоя святая святых. Только один поток в этот момент.
    _sharedList.Add(item); // Добавил и не обосрался
}

А нахуя он? Да потому что проще и быстрее него нихуя нет для большинства случаев. Главное — бери свой личный объект (_syncRoot), а не херачь lock(this). Иначе придут соседи, тоже захотят в твой сортир, и будет deadlock — все поссать хотят, а дверь не открывается.

2. SemaphoreSlim — турникет в метро Представь: есть узкий проход, и ты говоришь — «окей, больше пяти человек одновременно не пускаю». Идеально, когда нужно ограничить потоки, особенно если они асинхронные и не хочется блокировать всё нахуй.

private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5); // Пять — и хватит!

await _semaphore.WaitAsync(); // Ждём своей очереди как культурные
try
{
    await AccessLimitedResourceAsync(); // Делаем дела
}
finally
{
    _semaphore.Release(); // Выходим, даём место другим. Обязательно!
}

А нахуя он? Он легче своего старшего брата Semaphore и умеет ждать асинхронно (WaitAsync), не занимая попусту поток. Красота, а не инструмент.

3. ReaderWriterLockSlim — библиотека, блядь Ситуация: куча народа хочет почитать одну книжку (данные), но писатель (тот, кто меняет данные) заходит редко. Так вот, пусть читают все сразу, а когда писатель пришёл — все нахуй вышли, подождали, пока он свои правки внесёт, и снова пошли читать.

private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();

// Читатель зашёл
_rwLock.EnterReadLock();
try
{
    return _cache.Get(key); // Читает себе спокойно, пока другие тоже читают
}
finally
{
    _rwLock.ExitReadLock(); // Закрыл книжку, поставил на полку
}

// Писатель приперся
_rwLock.EnterWriteLock(); // Всё, трындец, читатели — свободны!
try
{
    _cache.Set(key, value); // Переписывает историю
}
finally
{
    _rwLock.ExitWriteLock(); // Вышел, теперь можно снова читать
}

А нахуя он? Когда чтения дохуя, а записи раз в год, он даёт овердохуищный прирост производительности. Не заставляй всех стоять в одной очереди из-за одного писаки.

4. Mutex — межгалактический семафор Это когда нужно договориться не только между потоками в одной программе, а между разными процессами, а то и между компами. Классика — чтобы второй экземпляр твоего приложения не запустился.

bool createdNew;
using var mutex = new Mutex(true, "Global\MyAppMutex", out createdNew);
if (!createdNew)
{
    // Ага, сука, кто-то уже сидит в этом кресле!
    return; // Сворачиваемся и идём нахуй.
}
// Мы первые, работаем спокойно...

А теперь, блядь, главные правила, которые надо выжечь на жопе:

  • Освобождай блокировки в finally всегда, даже если тебе кажется, что исключения не будет. Иначе залочишь навек — и приехали.
  • Сиди в критической секции как можно меньше. Зашёл, быстро сделал дело, вышел. Не надо там чай пить и с коллегами болтать.
  • Вложенные блокировки — это ад. Если без них нихуя, то договаривайся о строгом порядке захвата. Всегда сначала замок А, потом Б. Иначе два потока встанут в позу «ты мне — я тебе» и будут ждать вечность.
  • Для асинхронного кодаSemaphoreSlim и всякие ConcurrentBag, ConcurrentDictionary твои лучшие друзья. Они заточены под это дело. Не городи велосипед с lock в асинхронных методах, получишь боль и страдание.

Вот так вот, коротко и без воды. Выбирай инструмент по задаче, а не потому что он первый в гугле всплыл.