Всем привет. Сегодня я расскажу об инструментах, которые существуют в .NET для параллельной работы с какими-то внешними ресурсами и приведу примеры, где и как их можно применить.
При параллельной работе с каким-то ресурсом, нам нужно синхронизировать доступ к нему, чтобы не попасть в состояние гонки или банально не перегрузить его. Для этого в .NET существуют следующие вещи:
-
lock-object. Это самый простой способ синхронизации. Мы заводим объект, который будем использовать для блокировки параллельного выполнения какого-то участка кода.
-
Применять стоит, когда нам нужно, чтобы какой-то участок кода в один момент времени выполнялся только одним потоком. Тут может быть любая работа с файлами, БД и другими ресурсами.
-
Важно понимать, что
lockработает на уровне приложения, а не ОС, поэтому другое приложение может спокойно занять наш ресурс. -
Еще следует помнить, что код в
lockсекции должен выполняться в рамках одного потока, поэтому мы не можем использовать внутри асинхронные вызовы.class LockExample { private readonly object _lockObj = new(); public void Foo() { // Код, который может выполняться несколькими потоками lock (_lockObj) { // Код, выполняемый одним потоком } // Код, который может выполняться несколькими потоками } }
-
-
Mutex. Используется для ограничения доступа к ресурсу на уровне ОС.
-
Его может освободить только тот поток, который его занял.
-
Подойдет для ограничения доступа к файлам.
class MutexExample { public void Foo() { Mutex mtx = new(); // Код, который не требует работы со внешним ресурсом if (mtx.WaitOne()) // Можно указать таймаут { try { // Работа с каким-то ресурсом } finally { mtx.ReleaseMutex(); } } // Код, который может выполняться несколькими потоками } }
-
-
SemaphoreSlim. Облегченная версия семафора. Сам семафор предоставляется ОС и используется для того, чтобы ограничить число одновременных пользователей ресурса.
-
Если использовать не слим версию, то можем использовать для межпроцессорной синхронизации, так как работает на уровне ОС.
-
Можем указать, сколько одновременно потоков могут работать с ресурсом. Полезно, если мы не хотим перегрузить его, например, при обращении к сетевой карте при REST-запросах.
-
Слим версия может быть асинхронной, что полезно для работы с файлами, к которым мы хотим ограничить доступ. На работе некоторые настройки сервисов мы храним в
.json-файлах, для ограничения доступа к ним из нескольких потоков, мы используем слим версию и асинхронное ожидание.class SemSlimExample { private readonly SemaphoreSlime _sync = new(1, 1); public async Task FooAsync() { await _sync.WaitAsync(); try { // Код, который должен выполняться не более чем 1 потоком } finally { _sync.Release(); } } }
-
-
AutoResetEvent. Как и классы выше служит для синхронизации доступа к ресурсу.
-
Отличие в том, что позволяет управлять одним потоком из другого.
-
AutoResetEvent— автоматически возвращается в начальное состояние после сигнала.class AREExample { private AutoResetEvent _evt = new(false); private List<string> _data = new(); public void Foo() { _evt = new(false); var load = Task.Run(ReceiveDataFromServer); // Независимая от результата работа _evt.WaitOne(); // Работа с _data } private void ReceiveDataFromServer() { var rawData = Requester.GetData("url"); Parallel.ForEach((raw) => data.Add(HandleRaw(raw))); _evt.Set(); } } -
Из примера выше видно, что для подобных ситуаций сейчас проще использовать Task и async/await.
-
Еще существует
ManualResetEvent, который требуется возвращать в исходное состояние самостоятельно. -
Для более сложных сценариев существует
EventWaitHandle, но с ним мне не приходилось работать.
-
-
Interlocked. Служит для произведения атомарных операций.
-
Подходит, если есть какая-то переменная, которую мы хотим атомарно изменять.
-
Еще с его помощью можно поставить флаг на какую-то часть кода, которую должен выполнять какой-то из потоков, но нам не важно какой. Тогда при первом входе в метод поток будет поднимать флаг через Interlocked, а другие потоки будут выходить из метода, когда будут видеть, что флаг уже поднят.
class InterFlagExample { private int _flag = 0; public void Foo() { if (Interlocked.CompareExchange(ref _flag, value: 1, comparand: 0) != 0) return; // Работа для одного потока _flag = 0; } }
-
Это те вещи, которые я часто встречаю во время работы, и которые могут серьезно облегчить жизнь, если мы собираемся работать с каким-то ресурсом параллельно. Надеюсь кому-то эта статья поможет узнать что-то новое или разобраться с каким-то из этих способов синхронизации.
ссылка на оригинал статьи https://habr.com/ru/articles/784134/
Добавить комментарий