У меня есть приложение, которое использует шаблон производитель/потребитель для обработки событий (всего 2 потока). Основной поток (UI) атомарно записывает в поле ссылочного типа, а мой экземпляр потока атомарно считывает это поле. Вот демонстрационный код:
public class MyApp
{
private Thread WorkerThread; // Thread which processes the events.
private EventArgs QueuedEvent; // The latest event registered by the app.
private EventArgs ProcessedEvent; // The latest event processed by the worker.
public static void Main(string[] args)
{
// Create and start the thread.
WorkerThread = new(new ThreadStart(DoWork));
WorkerThread.Start();
// Attach event
[...]
}
// Event which is fired from the main thread.
// The timing of this event is unpredictable;
// it could be super fast, never fire, or anything in between.
private static void OnEvent(object sender, EventArgs e)
{
// Set this as the latest event.
// It is OK if the previous event is LOST.
QueuedEvent = e;
}
// Main loop for consumer thread.
private void DoWork()
{
while (true)
{
// Copy QueuedEvent to local variable so it's not overwritten.
var argsToWorkOn = QueuedEvent;
// For performance, don't do work if the args haven't changed.
if (argsToWorkOn != ProcessedEvent)
{
// Do expensive work with argsToWorkOn.
[...]
// Remember that we already did work with these args.
ProcessedEvent = argsToWorkOn;
}
}
}
}
Производительность в обоих потоках здесь является приоритетом №1; они не должны блокировать друг друга. Учитывая это, какой объективно наиболее эффективный способ реализовать потокобезопасность? Это:
volatile
здесь. Я читал аргументы за и против летучести, особенно в отношении современной архитектуры.@TheodorZoulias Да, это правда.
Какова ожидаемая частота событий? И каковы требования к задержке? Подобный бесконечный цикл съедает все ядро процессора и тратит немало энергии. Это может оказаться контрпродуктивным, если существует риск исчерпания доступных ядер.
В реальном цикле есть Thread.Sleep(1)
, который снижает загрузку процессора с ~ 12% до примерно 0,2%, по крайней мере, на моей машине. Событие считывает входные данные с аппаратного устройства, которые различаются от клиента к клиенту, что в конечном итоге зависит от того, насколько быстро устройство отправляет информацию. Может быть очень редко или, как обычно, очень быстро. Некоторые пользователи сообщали, что приложение медленно реагирует до этой реализации.
Каков здесь объективно наиболее эффективный способ реализации потокобезопасности?
Используйте ключевое слово изменчивый:
private volatile EventArgs QueuedEvent;
private volatile EventArgs ProcessedEvent;
Это абсолютный победитель с точки зрения использования вашего процессора и абсолютный проигрыш в производительности с точки зрения вашего собственного времени и здравомыслия. Вам потребуются месяцы обучения, особенно изучения исходного кода среды выполнения .NET, прежде чем путаница исчезнет и вы получите четкое понимание гарантий, которые volatile
дает вам на практике. И я говорю «на практике», потому что можно сказать, что, согласно официальной документации, сам исходный код .NET неверен, поскольку он делает предположения, которых в документации нет.
Короче говоря, ключевое слово volatile
гарантирует, что инструкции ЦП вашего кода не будут переупорядочены таким образом, чтобы один поток мог видеть экземпляр EventArgs
в частично инициализированном состоянии. Это также гарантирует, что компилятор не выдаст код, который считывает QueuedEvent
только один раз, а затем никогда не читает его снова, впоследствии всегда возвращая одно и то же кэшированное значение. Поток, читающий поле volatile QueuedEvent
, увидит достаточно свежее значение поля, примерно такое же свежее, как если бы вы использовали lock
или Interlocked
.
«После этого многомесячного исследования вы, скорее всего, придете к выводу, что ваш код в его нынешнем состоянии — это куча какашек» — какая-то жесткая любовь, — но я не согласен :D Я думаю, намекая, Карл было бы справедливо уделить некоторое внимание этому тесному циклу занятости (и избавиться от него).
Это подтверждает то, что я тогда читал о летучих, и это горько-сладко. Какие-то противные дела там! Что касается цикла занятости и кода «какашек», я бы хотел от них избавиться, поскольку это кажется немного излишним, но я не знаю, какая замена будет лучшей. Возможно, вы могли бы изменить свой ответ, который укажет мне правильное направление в этом случае?
@CarlSchmidt компоненты, наиболее часто используемые для реализации шаблона производитель-потребитель, — это BlockingCollection<T>
и Channel<T>
, первый — синхронный, а второй — асинхронный. Оба способны передавать миллионы товаров в секунду от производителя к потребителю. Если вам нужен совет о том, как реализовать конкретный сценарий производитель-потребитель, я бы предложил опубликовать об этом новый вопрос. Ожидается, что вопросы и ответы здесь будут одномерными и сосредоточены только на чем-то одном.
Кстати, правильно ли я понимаю, что пока
argsToWorkOn
равенProcessedEvent
,WorkerThread
выполняет цикл занятости?