#c_sharp #wpf #многопоточность #task
Есть у меня простой класс-логгер: 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 "Вызывающий поток не может получить доступ к данному объекту, так как владельцем этого объекта является другой поток." Как это грамотно и с малой кровью исправить?
Ответы
Ответ 1
Смотрите. Проблема в том, что события доставляются в том потоке, который отправляет события. Поэтому у вас подписчики событий получают событие каждый раз в разных потоках. Если подписчик — UI-код, который просто выводит текст в UI, то при приходе сообщения из неглавного потока происходит проблема. Есть несколько путей починки вашего кода. Можно привязать логгер к главному потоку. При этом сообщения будут доставляться только в главном потоке, и соответственно UI-код будет всегда работать «как надо». static class Logger { static Lazydispatcher = new Lazy (() => 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 (что не позволит использовать её повторно в других программах). Зато этот метод решает задачу наиболее просто: другие переделки при этом не нужны. Можно считать логгер не привязанным ни к какому потоку, тогда UI-код должен проверять, в каком потоке он запущен, и при необходимости пользоваться Dispatcher.InvokeAsync. Logger.OnMessage += s => { if (Dispatcher.CheckAccess()) LogContainer.Text += (s + "\n"); else Dispatcher.InvokeAsync(() => LogContainer.Text += (s + "\n")); }; Это более правильный подход, но здесь придётся потенциально править все места, где происходит подписка на сообщения от логгера. Впрочем, такое место в программе, судя по всему, одно. Вы можете использовать модные в этом сезоне Reactive Extensions, и переписать ваш класс на них: using System.Reactive.Linq; using System.Reactive.Subjects; static class Logger { static ISubject subject = Subject.Synchronize(new Subject ()); public static IObservable 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.
Комментариев нет:
Отправить комментарий