Передача сообщений между потоками - C#

752
09 декабря 2016, 08:52

Есть у меня простой класс-логгер:

static class Logger
{
    public delegate void Message(string msg);
    static public event Message OnMessage;
    static public void SendMessage(string msg)
    {
        OnMessage?.Invoke(msg);
    }
}

Я из любых мест приложения отправляю ему сообщения по типу: Logger.SendMessage("Получена команда на запуск");

При загрузке window (WPF) я подписываюсь на события логгера и вывожу лог в textbox

Пока приложение было однопоточным всё отлично работало, но теперь методы отлажены и надо всё распараллелить (идет обращение к 40 БД на разных хостах поэтому всё хорошо параллелится)

Но теперь возникла проблема - при попытке прочитать отправленное сообщение из другого потока возникает Exception

"Вызывающий поток не может получить доступ к данному объекту, так как владельцем этого объекта является другой поток."

Как это грамотно и с малой кровью исправить?

Answer 1

Смотрите.

Проблема в том, что события доставляются в том потоке, который отправляет события. Поэтому у вас подписчики событий получают событие каждый раз в разных потоках. Если подписчик — UI-код, который просто выводит текст в UI, то при приходе сообщения из неглавного потока происходит проблема.

Есть несколько путей починки вашего кода.

  1. Можно привязать логгер к главному потоку. При этом сообщения будут доставляться только в главном потоке, и соответственно UI-код будет всегда работать «как надо».

    static class Logger
    {
        static Lazy<Dispatcher> dispatcher =
            new Lazy<Dispatcher>(() => Application.Current.Dispatcher);
        public delegate void Message(string msg);
        static public event Message OnMessage;
        static public void SendMessage(string msg)
        {
            if (dispatcher.Value.CheckAccess())
                OnMessage?.Invoke(msg);
            else
                dispatcher.Value.InvokeAsync(() => OnMessage?.Invoke(msg));
        }
    }
    

    Это, наверное, не самое лучшее архитектурное решение, т. к. при этом логгер получается зависимым от WPF, то есть модель получает зависимость от VM (что не позволит использовать её повторно в других программах). Зато этот метод решает задачу наиболее просто: другие переделки при этом не нужны.

  2. Можно считать логгер не привязанным ни к какому потоку, тогда UI-код должен проверять, в каком потоке он запущен, и при необходимости пользоваться Dispatcher.InvokeAsync.

    Logger.OnMessage += s =>
        {
            if (Dispatcher.CheckAccess())
                LogContainer.Text += (s + "\n");
            else
                Dispatcher.InvokeAsync(() => LogContainer.Text += (s + "\n"));
        };
    

    Это более правильный подход, но здесь придётся потенциально править все места, где происходит подписка на сообщения от логгера. Впрочем, такое место в программе, судя по всему, одно.

  3. Вы можете использовать модные в этом сезоне Reactive Extensions, и переписать ваш класс на них:

    using System.Reactive.Linq;
    using System.Reactive.Subjects;
    static class Logger
    {
        static ISubject<string> subject = Subject.Synchronize(new Subject<string>());
        public static IObservable<string> Messages => subject;
        static public void SendMessage(string msg) => subject.OnNext(msg);
    }
    

    Подписка при этом выглядит так:

    Logger.Messages.ObserveOnDispatcher().Subscribe(s => LogContainer.Text += (s + "\n"));
    

    Максимальная гибкость, LINQ на сообщениях, доставка в произвольный поток, навесные плюшки наподобие подавления слишком частых или повторяющихся сообщений поставляется в комплекте, бонусом ощущение собственной крутости, функциональности и трендовости. Минус — вам придётся-таки разобраться с этим самым Rx (муа-ха-ха!). Или это можно считать плюсом, да. (Think positive.)

    Не забудьте подключить из nuget System.Reactive.Core, System.Reactive.Interfaces, System.Reactive.Linq и System.Reactive.Windows.Threading.

READ ALSO
Поиск элемента по имени в WinForms

Поиск элемента по имени в WinForms

Мне надо на форме именно найти элемент (button, panel и тд

643
Реализация ввода десятичных цифр

Реализация ввода десятичных цифр

Всем доброго! Народ помогите новичку никак не могу решить вопрос вводы десятичных цифрКак это делать правильно

279
RabbitMQ: вопрос оптимизации очередей

RabbitMQ: вопрос оптимизации очередей

Я новичок в RabbitMQПрочитал статью «Deploying Microservices Architecture with C#, Part 2»

296
Стоит задача оптимизации кода C#

Стоит задача оптимизации кода C#

Столкнулся с проблемой в Windows FormПример: Создано две кнопки Btn1, Btn2 и textBox1

319