Ответ
В .NET существует иерархия примитивов синхронизации для управления доступом к общим ресурсам из нескольких потоков. Выбор зависит от сценария и требуемого уровня производительности.
1. Низкоуровневые (на основе атомарных операций)
- Ключевое слово
volatile: Гарантирует, что чтение и запись в поле будут атомарными, а компилятор или процессор не переупорядочат операции с ним. Не является полноценным примитивом синхронизации. - Класс
Interlocked: Предоставляет атомарные операции для простых действий с числами и ссылками.int sharedCounter = 0; // Атомарное увеличение Interlocked.Increment(ref sharedCounter); // Атомарное сравнение и обмен (CAS - Compare-And-Swap) int oldValue, newValue; do { oldValue = sharedCounter; newValue = oldValue + 10; } while (Interlocked.CompareExchange(ref sharedCounter, newValue, oldValue) != oldValue);
2. Примитивы на основе ядра ОС (медленные, но мощные)
Используют переход в режим ядра, что дорого. Подходят для синхронизации между процессами.
Mutex(мьютекс): Примитив взаимоисключения. Может быть именованным для межпроцессного взаимодействия.SemaphoreиSemaphoreSlim: Ограничивают количество потоков, которые могут одновременно получить доступ к ресурсу.EventWaitHandle(AutoResetEvent,ManualResetEvent): Позволяют потоку сигнализировать другим потокам о наступлении события.
3. Гибридные и пользовательские примитивы (быстрые для внутрипроцессной синхронизации)**
Сначала пытаются выполнить операцию в пользовательском режиме, при конфликте переходят в режим ядра.
lock(ключевое слово) /Monitor: Самый распространенный примитив для обеспечения эксклюзивного доступа к блоку кода.private readonly object _syncRoot = new object(); public void ThreadSafeMethod() { lock (_syncRoot) // Вход в критическую секцию { // Работа с общим ресурсом } }ReaderWriterLockSlim: Оптимизирован для сценариев, где много операций чтения и мало записи. Позволяет множественное чтение, но эксклюзивную запись.ManualResetEventSlimиSemaphoreSlim: Облегченные версии своих аналогов на основе ядра, рекомендуются для использования внутри одного процесса.
Общее правило: Начинайте с самых "легковесных" примитивов (Interlocked, lock). Переходите к более сложным (ReaderWriterLockSlim, SemaphoreSlim) только при доказанной необходимости и понимании их семантики.
Ответ 18+ 🔞
Смотри, в .NET есть целая куча разных штук, чтобы потоки не подрались за общие данные. Выбирать надо по уму, а то можно такую ботву написать, что потом самому разгребать.
1. Самые простые, на уровне процессора (быстрые, но для простых случаев)
- Ключевое слово
volatile: Это не волшебная таблетка от всех проблем, а так, намёк компилятору. Говорит: "Э, дружок, не переставляй операции с этим полем местами и всегда читай из памяти, а не из своего кэша". Для синхронизации одного его — маловато будет. - Класс
Interlocked: Вот это уже дело. Позволяет делать простые операции (прибавить, сравнить и поменять) так, чтобы это было атомарно, то есть неразрывно. Прямо как на уровне процессорной инструкции.int sharedCounter = 0; // Атомарно плюсанули на единичку Interlocked.Increment(ref sharedCounter); // Атомарное "сравнял и если ок, то подменил" (CAS) int oldValue, newValue; do { oldValue = sharedCounter; newValue = oldValue + 10; } while (Interlocked.CompareExchange(ref sharedCounter, newValue, oldValue) != oldValue);Быстро, чётко. Но если логика сложная — уже не натянешь.
2. Тяжёлая артиллерия, через ядро ОС (медленные, но всесильные)
Тут уже серьёзно. Поток, если не может пройти, засыпает, и управление уходит в ядро операционки. Это овердохуища накладных расходов, зато могут синхронизировать даже разные процессы.
Mutex(мьютекс): Как один ключ от туалета на весь офис. Кто ключ взял — тот и царь. Можно даже именованный сделать, чтобы между разными программами синхронизироваться.SemaphoreиSemaphoreSlim: Как турникет в метро. Запускает одновременно только N потоков. Остальные ждут в очереди.EventWaitHandle(AutoResetEvent,ManualResetEvent): Как кнопка "вызова" лифта. Один поток нажал — сигнал пошёл, другие проснулись.AutoResetEventсам сбрасывается после пробуждения одного,ManualResetEvent— как флажок, пока вручную не опустишь.
3. Гибридные штуки (умные и обычно быстрые)
Хитрые ребята. Сначала пытаются договориться в пользовательском режиме (быстро), а если не получается — ну тогда уже идут в ядро (медленно). Идеально для драк внутри одного процесса.
lock(ключевое слово) /Monitor: Классика жанра, хлеб с маслом. Просто оборачиваешь кусок кода, и только один поток в него может зайти. Остальные тупо ждут.private readonly object _syncRoot = new object(); public void ThreadSafeMethod() { lock (_syncRoot) // Захватили замок { // Делаем что-то с общим ресурсом, не опасаясь } // Замок сам отпустится, даже если вылетит исключение }ReaderWriterLockSlim: Для умных сценариев, где много кто читает, но редко кто-то пишет. Позволяет пускать всех читателей одновременно, а вот писателя — только одного и в полной тишине. Экономит время, если читателей дохуя.ManualResetEventSlimиSemaphoreSlim: То же самое, что их старшие братья из пункта 2, но облегчённые, заточенные под работу в рамках одного процесса. Почти всегда бери их, а не те.
Главный совет, как не выстрелить себе в ногу: Не гонись за сложным. Начинай с самого простого, что решает задачу — Interlocked для счётчиков, lock для кусков кода. Потом, если упрёшься в конкретные проблемы производительности и точно понимаешь, что тебе нужно (например, разделение на читателей и писателей), тогда уже хватай ReaderWriterLockSlim или SemaphoreSlim. А межпроцессные мьютексы — это вообще на крайний случай, когда уже совсем припёрло.