Ответ
Безопасная блокировка (синхронизация) доступа к общему ресурсу из нескольких потоков — ключевая задача. В C# для этого есть несколько механизмов, выбор зависит от контекста.
Основные подходы:
-
Ключевое слово
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; } } }
-
Класс
Mutex(мьютекс)- Принцип: Аналогичен
lock, но может быть именованным и использоваться для синхронизации между разными процессами (межпроцессная синхронизация). - Минусы: Более тяжеловесный, чем
lock.private static Mutex _mutex = new Mutex(false, "GlobalMyAppMutex"); public void Process() { _mutex.WaitOne(); // Захват мьютекса try { // Критическая секция } finally { _mutex.ReleaseMutex(); // Освобождение мьютекса ВСЕГДА в finally } }
- Принцип: Аналогичен
-
Класс
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(); // Освобождаем слот } }
- Принцип: Позволяет ограничить количество потоков, которые могут одновременно войти в критическую секцию, до заданного числа (больше 1).
-
Класс
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() или другие асинхронные примитивы, чтобы не блокировать потоки пула.
Ответ 18+ 🔞
Давай разберем эту тему про то, как потокам не давать друг другу в табло, когда они лезут к одному и тому же ресурсу. В C# для этого есть целый арсенал, и выбор зависит от того, какую именно драку ты хочешь устроить.
Основные способы:
-
Ключевое слово
lock(по сути, обёртка надMonitor)- Самый простой и частый способ, когда все действо происходит в рамках одного приложения.
- Суть: Берёшь какой-нибудь объект как маркер (
object _lock = new object();). Кто первый схватил этот маркер — тот и работает. Остальные ждут, как лохи. -
Важнейшее правило: Используй приватный, ридонли объект, созданный специально для блокировки. Ни в коем случае не бери
this,typeof(...)или что-то публичное — это верный путь к взаимной блокировке, где все потоки просто повиснут и сдохнут.public class Account { private readonly object _balanceLock = new object(); // Вот он, наш маркер для драки private decimal _balance; public void Deposit(decimal amount) { // Только один поток в один момент времени может тут орудовать lock (_balanceLock) { _balance += amount; // Спокойно работаем, пока остальные стоят в очереди } } }
-
Класс
Mutex- Похож на
lock, но круче. Его можно именовать и использовать для синхронизации между разными процессами (например, чтобы два экземпляра твоей программы не накосячили с одним файлом). - Минус: Он тяжелее, чем обычный
lock, так что внутри одного процесса без нужды не юзай.private static Mutex _mutex = new Mutex(false, "GlobalMyAppMutex"); public void Process() { _mutex.WaitOne(); // Захватываем мьютекс. Если он уже занят — висим и ждём. try { // Делаем что-то важное в полной изоляции } finally { _mutex.ReleaseMutex(); // ОСВОБОЖДАТЬ НАДО В ЛЮБОМ СЛУЧАЕ, даже если вылетело исключение! } }
- Похож на
-
SemaphoreиSemaphoreSlim- Тут философия другая.
lockиMutexпускают только одного. А семафор может пустить сразу несколько потоков, но не больше заданного лимита.SemaphoreSlim— его облегчённый брат для работы внутри одного процесса. - Идеально, когда у тебя, например, лимит на 5 одновременных запросов к какому-нибудь API, и больше нельзя.
private static SemaphoreSlim _pool = new SemaphoreSlim(initialCount: 3, maxCount: 3); // Пускаем максимум троих
public async Task AccessResourceAsync() { await _pool.WaitAsync(); // Ждём, если все три слота уже заняты try { // Работаем с ресурсом. Нас тут может быть до трёх штук одновременно. await Task.Delay(1000); } finally { _pool.Release(); // Освобождаем слот, чтобы кто-то другой мог зайти } }
- Тут философия другая.
-
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(); } }
Главный принцип, который забывают идиоты: Всегда освобождай блокировку в finally! Иначе при исключении твой ресурс намертво повиснет, и остальные потоки будут ждать его до скончания времён. Для асинхронного кода используй асинхронные версии (типа SemaphoreSlim.WaitAsync()), чтобы не блокировать потоки пула попусту.