Атомная безопасность потоков Производитель/потребитель

У меня есть приложение, которое использует шаблон производитель/потребитель для обработки событий (всего 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 здесь. Я читал аргументы за и против летучести, особенно в отношении современной архитектуры.
  • Использовать блокировку? Может быть. Основной поток может блокироваться каждый раз, когда рабочий поток читает поле, а это плохо.
  • Использовать взаимосвязанный? Я не думаю, что это очень применимо здесь, но я могу ошибаться.
  • Использовать ReaderWriterLockSlim? На самом деле я еще не углублялся в это, я просто знаю, что оно существует.
  • Что-то другое?

Кстати, правильно ли я понимаю, что пока argsToWorkOn равен ProcessedEvent, WorkerThread выполняет цикл занятости?

Theodor Zoulias 17.04.2024 05:23

@TheodorZoulias Да, это правда.

Carl Schmidt 17.04.2024 05:34

Какова ожидаемая частота событий? И каковы требования к задержке? Подобный бесконечный цикл съедает все ядро ​​процессора и тратит немало энергии. Это может оказаться контрпродуктивным, если существует риск исчерпания доступных ядер.

JonasH 17.04.2024 09:00

В реальном цикле есть Thread.Sleep(1), который снижает загрузку процессора с ~ 12% до примерно 0,2%, по крайней мере, на моей машине. Событие считывает входные данные с аппаратного устройства, которые различаются от клиента к клиенту, что в конечном итоге зависит от того, насколько быстро устройство отправляет информацию. Может быть очень редко или, как обычно, очень быстро. Некоторые пользователи сообщали, что приложение медленно реагирует до этой реализации.

Carl Schmidt 17.04.2024 17:18
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
58
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Каков здесь объективно наиболее эффективный способ реализации потокобезопасности?

Используйте ключевое слово изменчивый:

private volatile EventArgs QueuedEvent;
private volatile EventArgs ProcessedEvent;

Это абсолютный победитель с точки зрения использования вашего процессора и абсолютный проигрыш в производительности с точки зрения вашего собственного времени и здравомыслия. Вам потребуются месяцы обучения, особенно изучения исходного кода среды выполнения .NET, прежде чем путаница исчезнет и вы получите четкое понимание гарантий, которые volatile дает вам на практике. И я говорю «на практике», потому что можно сказать, что, согласно официальной документации, сам исходный код .NET неверен, поскольку он делает предположения, которых в документации нет.

Короче говоря, ключевое слово volatile гарантирует, что инструкции ЦП вашего кода не будут переупорядочены таким образом, чтобы один поток мог видеть экземпляр EventArgs в частично инициализированном состоянии. Это также гарантирует, что компилятор не выдаст код, который считывает QueuedEvent только один раз, а затем никогда не читает его снова, впоследствии всегда возвращая одно и то же кэшированное значение. Поток, читающий поле volatile QueuedEvent, увидит достаточно свежее значение поля, примерно такое же свежее, как если бы вы использовали lock или Interlocked.

«После этого многомесячного исследования вы, скорее всего, придете к выводу, что ваш код в его нынешнем состоянии — это куча какашек» — какая-то жесткая любовь, — но я не согласен :D Я думаю, намекая, Карл было бы справедливо уделить некоторое внимание этому тесному циклу занятости (и избавиться от него).

Fildor 17.04.2024 08:42

Это подтверждает то, что я тогда читал о летучих, и это горько-сладко. Какие-то противные дела там! Что касается цикла занятости и кода «какашек», я бы хотел от них избавиться, поскольку это кажется немного излишним, но я не знаю, какая замена будет лучшей. Возможно, вы могли бы изменить свой ответ, который укажет мне правильное направление в этом случае?

Carl Schmidt 17.04.2024 17:29

@CarlSchmidt компоненты, наиболее часто используемые для реализации шаблона производитель-потребитель, — это BlockingCollection<T> и Channel<T>, первый — синхронный, а второй — асинхронный. Оба способны передавать миллионы товаров в секунду от производителя к потребителю. Если вам нужен совет о том, как реализовать конкретный сценарий производитель-потребитель, я бы предложил опубликовать об этом новый вопрос. Ожидается, что вопросы и ответы здесь будут одномерными и сосредоточены только на чем-то одном.

Theodor Zoulias 17.04.2024 19:47

Другие вопросы по теме