#c# #multithreading #.net-core #backgroundworker
#c# #многопоточность #.net-ядро #backgroundworker #.net-core
Вопрос:
У меня есть фоновая служба IHostedService
в ядре dotnet 3.1, которая принимает запросы от 100 клиентов (машин на заводе) с использованием сокетов (домашняя версия). Моя проблема в том, что в разных потоках может поступать несколько вызовов к одному и тому же методу в классе, который имеет доступ к объекту (общему состоянию). Это распространено в кодовой базе. Запросы также должны обрабатываться в правильном порядке.
Причина, по которой этого нет в базе данных, связана с соображениями производительности (система реального времени). Я знаю, что могу использовать блокировку, но я не хочу иметь блокировки по всей базе кода.
Каков стандартный способ справиться с этой ситуацией. Используете ли вы базу данных в памяти? Кэш в памяти? Или мне просто нужно везде добавлять блокировки?
public class Machine
{
public MachineState {get; set;}
// Gets called by multiple threads from multiple clients
public bool CheckMachineStatus()
{
return MachineState.IsRunning;
}
// Gets called by multiple threads from multiple clients
public void SetMachineStatus()
{
MachineState = Stopped;
}
}
Обновить
Вот пример. У меня есть консольное приложение, которое взаимодействует с машиной через сокеты для взвешивания продуктов. При инициализации консольного приложения данные будут загружены в память (информация о взвешиваемых продуктах). Все это делается в главном потоке для сохранения целостности данных.
Когда поступает вызов от взвешивающего устройства в потоке 1, он переключается на основной поток для доступа к информации о продукте и завершения любой другой работы, такой как создание событий для других частей системы.
В настоящее время это переключение с потока 1,2, … N на основной поток выполняется с помощью встроенного решения и было сделано, чтобы избежать блокировки кода по всей кодовой базе. Это было написано в .Net 1.1 и после перехода на ядро dotnet 3.1. Я подумал, что может быть фреймворк, библиотека, инструмент, техника и т.д., Которые могли бы справиться с этим для нас, или просто лучший способ.
Это существующая система, которую я все еще изучаю. Надеюсь, это имеет смысл.
Комментарии:
1. Какие данные вы сохраняете? Является ли это реляционным? Можете ли вы поместить его в хранилище ключей / значений? Вы получите лучший ответ, если предоставите больше контекста.
2. Производительность и атомарная согласованность обычно являются компромиссом между тем или иным. Если вы не хотите использовать блокировку, вы можете посмотреть на
System.Threading.Interlocked
класс для некоторых операций, которые могут быть выполнены атомарно. Если все, что вы делаете, это считываете / обновляете логическую переменную из нескольких потоков, класс Interlocked позволит вам сделать это без необходимости блокировки. Если ваш вариант использования более сложный, вам нужно описать его, иначе мы не сможем вам сильно помочь3. @insane_developer Данные могут быть любыми: объектом, списком объектов, рядом переменных. Я знаю, что могу использовать что-то вроде EF core in-memory provider, Ravendb или Litedb среди других. Поскольку я новичок в этом домене (исходя из веб-фона) Я думал, что будет стандартный способ справиться с этим сценарием. Я понимаю, что это немного общий вопрос, и я попытаюсь обновить свой пример. Спасибо.
4. @AndrewWilliamson Да, у нас есть код, который обрабатывает атомарную согласованность, как вы упомянули Lock, Interlocked и ManualResetEvent. Я хотел посмотреть, есть ли способ получше. Спасибо.
5. Возможно, я неправильно понимаю вашу проблему, но если вам нужно обрабатывать многопоточные запросы в хронологическом порядке, не будет ли шаблон «Несколько производителей, один потребитель» значительно упростить ситуацию? Пусть ваши обработчики сокетов сбрасывают «команды» в конце одной потокобезопасной коллекции, затем завершите работу. Другой поток действует как процессор, последовательно извлекая элементы из заголовка коллекции и выполняя их по одному в порядке их получения.
Ответ №1:
Использование базы данных в памяти — это вариант, если вы готовы делегировать все ситуации, вызывающие параллелизм, базе данных и ничего не делать с помощью кода. Например, если вам необходимо обновить значение в базе данных в зависимости от какого-либо условия, то условие должно проверяться базой данных, а не вашим собственным кодом.
Добавление блокировок везде также является вариантом, который почти наверняка приведет к неуправляемому коду довольно быстро. Код, вероятно, будет пронизан скрытыми ошибками с самого начала, ошибками, которые вы будете обнаруживать один за другим с течением времени, обычно при самых неблагоприятных обстоятельствах.
Вы должны понимать, что имеете дело со сложной проблемой, волшебных решений которой нет. Управление общим состоянием в многопоточном приложении всегда было источником проблем.
Я предлагаю инкапсулировать всю эту сложность внутри потокобезопасных классов, которые могут безопасно вызывать остальные приложения. То, как вы делаете эти классы потокобезопасными, зависит от ситуации.
-
Использование блокировок является наиболее гибким вариантом, но не всегда наиболее эффективным, поскольку это может привести к конфликту.
-
Использование потокобезопасных коллекций, таких как
ConcurrentDictionary
, например, менее гибко, поскольку гарантии потокобезопасности, которые они предлагают, ограничены целостностью их внутреннего состояния. Если, например, вы должны обновить одну коллекцию на основе условия, полученного из другой коллекции, то всю операцию нельзя сделать атомарной, просто используя коллекции безопасности потоков. С другой стороны, эти коллекции обеспечивают лучшую производительность, чем простые замки. -
Использование неизменяемых коллекций, как
ImmutableQueue
, например, является еще одним интересным вариантом. Они менее эффективны как с точки зрения памяти, так и с точки зрения процессора, чем параллельные коллекции (добавление / удаление во многих случаях выполняется O (Log n) вместо O (1)) и не более гибкие, чем они, но они очень эффективны, особенно при предоставлении снимков активно обрабатываемых данных. Для атомарного обновления неизменяемой коллекции существует удобныйImmutableInterlocked.Update
метод. Он обновляет ссылку на неизменяемую коллекцию обновленной версией той же коллекции без использования блокировок. В случае конфликта с другими потоками он может вызывать предоставленное преобразование несколько раз, пока не выиграет гонку.
Комментарии:
1. Спасибо за ответ. Да, я хотел отключить загрузку всех ситуаций параллелизма в базу данных. Я подумал, что в этом сценарии может использоваться общая платформа / инструмент или база данных. Что-то вроде Redis, кэша памяти ядра Dotnet или других хранилищ в памяти.
2. @ctb
MemoryCache
— это просто кэш. Он предлагает политики истечения срока действия и тому подобное, а также потокобезопасность для обновления отдельных ключей, но сам по себе не обеспечивает атомарность, когда вы хотите обновить ключ на основе значения другого ключа. Поэтому, если вы хотите сделать что-то нетривиальное, вам потребуется дополнительная синхронизация (блокировки). О Redis я не могу комментировать, потому что я мало что знаю об этом. Говоря о базах данных в памяти, я имел в виду OLTP-сервер SQL Server в памяти, который может запускать хранимые процедуры, поэтому он должен хорошо справляться с параллелизмом.3. Вы также можете посмотреть на каналы<T> или поток данных TPL для определения поведения конвейера / очереди в памяти (или pub-sub). Это высокопроизводительные структуры данных, полностью потокобезопасные и созданные для асинхронного программирования. Вы также могли бы взглянуть на реактивные расширения Rx.
4. Я выбрал каналы, чтобы у нас было много производителей и один потребитель. Кажется, лучший способ справиться с этим — использовать один поток. Возможно, я использую Rx позже, но хотел бы избежать этой сложности, если это возможно.
5. @ctb
Channels
— это удобный маленький инструмент, который также обеспечивает превосходную эффективность, но он не является таким полным решением, как поток данных TPL. Вам нужно перекачивать элементы из одного канала в другой вручную, в то время как поток данных TPL делает это автоматически. Вы просто объявляете блоки потока данных и связываете их вместе, а затем данные передаются сами по себе.