Шаблон проектирования для асинхронных вызовов в C # - PullRequest
5 голосов
/ 03 января 2011

Я разрабатываю настольное приложение с несколькими уровнями: уровень GUI (WinForms MVP) содержит ссылки на интерфейсы классов адаптеров, и эти адаптеры вызывают классы BL, которые выполняют реальную работу.

Помимо выполненияЗапросы от GUI, BL также запускает некоторые события, на которые GUI может подписаться через интерфейсы.Например, в BL есть объект CurrentTime, который периодически меняется, и графический интерфейс должен отражать эти изменения.

Есть две проблемы, связанные с многопоточностью:

  1. Мне нужно сделатьнекоторые логические вызовы асинхронны, чтобы не блокировать графический интерфейс.
  2. Некоторые события, которые получаются от GUI, запускаются не из потоков GUI.

На каком уровне лучше всего обрабатывать многопоточность?Моя интуиция говорит, что для этого больше всего подходит докладчик, я прав?Можете ли вы дать мне пример приложения, которое делает то, что мне нужно?И имеет ли смысл для докладчика удерживать ссылку на форму, чтобы он мог вызывать на нее делегатов?

РЕДАКТИРОВАТЬ: Щедрость, вероятно, достанется Хенрику, если только кто-то не дастлучший ответ.

Ответы [ 4 ]

5 голосов
/ 03 января 2011

Я бы посмотрел на использование BLL на основе Task для тех частей, которые можно описать как «фоновые операции» (то есть они запускаются пользовательским интерфейсом и имеют определенную точку завершения). Visual Studio Async CTP включает в себя документ, описывающий асинхронный шаблон на основе задач (TAP);Я рекомендую проектировать ваш BLL API таким способом (хотя языковые расширения async / await еще не выпущены).

Для частей вашей BLL, которые являются «подписками» (то естьони запускаются пользовательским интерфейсом и продолжаются бесконечно), есть несколько вариантов (в порядке моего личного предпочтения):

  1. Используйте API на основе Task, но с TaskCompletionSource, которыйникогда не завершается (или завершается только отменой как часть завершения работы приложения).В этом случае я рекомендую написать свои собственные IProgress<T> и EventProgress<T> (в Async CTP): IProgress<T> дает вашему BLL интерфейс для отчетов о прогрессе (замена событий прогресса), а EventProgress<T> обрабатывает захват SynchronizationContextдля маршалинга делегата «сообщить о ходе выполнения» в поток пользовательского интерфейса.
  2. Использовать Rx's IObservable framework;это хорошее соответствие с точки зрения дизайна, но имеет довольно крутой график обучения и менее стабильно, чем мне лично нравится (это библиотека перед выпуском).
  3. Используйте старомодный асинхронный шаблон на основе событий (EAP)), где вы фиксируете SynchronizationContext в своем BLL и инициируете события, ставя их в очередь в этот контекст.

РЕДАКТИРОВАТЬ 2011-05-17: С момента написания вышеупомянутогоКоманда Async CTP заявила, что подход (1) не рекомендуется (так как он несколько нарушает систему «отчетов о проделанной работе»), и команда Rx выпустила документацию, которая разъясняет их семантику.Теперь я рекомендую Rx для подписок.

2 голосов
/ 03 января 2011

Это зависит от того, какое приложение вы пишете, например, принимаете ли вы ошибки?Каковы ваши требования к данным - софт в реальном времени?кислота?в конечном итоге согласованные и / или частично подключенные / иногда отключенные клиенты?

Остерегайтесь различий между параллелизмом и асинхронностью.Вы можете иметь асинхронность и, следовательно, вызывать чередование вызовов методов, фактически не имея одновременно выполняемой программы.

Одна из идей может заключаться в том, чтобы иметь сторону чтения и записи вашего приложения, где сторона записи публикует события, когда оно было изменено.,Это может привести к созданию системы, управляемой событиями - сторона чтения будет построена на основе опубликованных событий и может быть перестроена.Пользовательский интерфейс может быть управляемым задачей - в этом случае выполнение задачи приведет к созданию команды, которую будет выполнять ваш BL (или уровень домена, если вы того пожелаете).

Логический следующий шаг, если у вас есть вышеуказанное,это также пойти на основе событий.Затем вы воссоздали бы внутреннее состояние модели записи через то, что было зафиксировано ранее.Существует группа Google по CQRS / DDD, которая может помочь вам в этом.

Что касается обновления пользовательского интерфейса, я обнаружил, что интерфейсы IObservable в System.Reactive, System.Interactive, System.CoreEx хорошо работают.подходит.Он позволяет вам пропускать различные контексты одновременного вызова - диспетчер - пул потоков и т. Д., И он хорошо взаимодействует с библиотекой параллельных задач.

Вы также должны учитывать, куда вы помещаете свою бизнес-логику - есливы работаете с доменом, я бы сказал, что вы можете поместить его в свое приложение, так как у вас будет процедура обновления для двоичных файлов, которые вы распространяете в любом случае, когда придет время обновления, но есть также возможность разместить его на сервере.Команды могут быть хорошим способом выполнения обновлений на стороне записи и удобной единицей работы при сбое кода, ориентированного на соединение (они небольшие и сериализуются, а пользовательский интерфейс может быть сконструирован вокруг них).

Комуприведем пример, взгляните на этот поток , с этим кодом, который добавляет приоритет IObservable.ObserveOnDispatcher (...) - вызов:

    public static IObservable<T> ObserveOnDispatcher<T>(this IObservable<T> observable, DispatcherPriority priority)
    {
        if (observable == null)
            throw new NullReferenceException();

        return observable.ObserveOn(Dispatcher.CurrentDispatcher, priority);
    }

    public static IObservable<T> ObserveOn<T>(this IObservable<T> observable, Dispatcher dispatcher, DispatcherPriority priority)
    {
        if (observable == null)
            throw new NullReferenceException();

        if (dispatcher == null)
            throw new ArgumentNullException("dispatcher");

        return Observable.CreateWithDisposable<T>(o =>
        {
            return observable.Subscribe(
                obj => dispatcher.Invoke((Action)(() => o.OnNext(obj)), priority),
                ex => dispatcher.Invoke((Action)(() => o.OnError(ex)), priority),
                () => dispatcher.Invoke((Action)(() => o.OnCompleted()), priority));
        });
    }

ПримерВыше можно использовать, как это запись в блоге обсуждает

public void LoadCustomers()
{
    _customerService.GetCustomers()
        .SubscribeOn(Scheduler.NewThread)
        .ObserveOn(Scheduler.Dispatcher, DispatcherPriority.SystemIdle)
        .Subscribe(Customers.Add);
}

... Так, например, с виртуальным магазином Starbucks, у вас будет доменная сущность, которая имеет что-то вроде 'Barista'class, который производит события' CustomerBoughtCappuccino ': {cost:' $ 3 ', отметка времени:' 2011-01-03 12: 00: 03.334556 GMT + 0100 ', ... и т. д.}.Ваша читающая сторона подпишется на эти события.Сторона чтения может быть некоторой моделью данных - для каждого из ваших экранов, которые представляют данные - представление будет иметь уникальный класс ViewModel - который будет синхронизирован с представлением в наблюдаемом словаре , например ,Хранилище будет (: IObservable), и ваши докладчики подпишутся на все это или только на его часть.Таким образом, ваш графический интерфейс может быть:

  1. Управляемый задачами -> Управляемый BL, с акцентом на пользовательские операции
  2. Асинхронный
  3. Разделение на чтение-запись

Учитывая, что ваш BL принимает только команды и не отображает «модель, достаточную для всех страниц» типа чтения, вы можете сделать в нем большинство вещей внутренними, внутренними защищенными и частными.Это означает, что вы можете использовать System.Contracts, чтобы доказать, что в нем нет ошибок (!).Это произвело бы события, которые прочитала бы ваша модель чтения.Вы можете взять основные принципы из Caliburn Micro об организации рабочих процессов выполняемых асинхронных задач (IAsyncResults).

Существуют некоторые Rx рекомендации по проектированию , которые вы можете прочитать.И cqrsinfo.com о поиске событий и cqrs.Если вы действительно заинтересованы в том, чтобы выйти за пределы сферы асинхронного программирования в сферу параллельного программирования, Microsoft выпустила бесплатную книгу о том, как программировать такой код.

Надеюсь, это поможет.

1 голос
/ 03 января 2011

Я бы рассмотрел «Шаблон посредника нити прокси».Пример здесь CodeProject

Практически все вызовы методов ваших адаптеров выполняются в рабочем потоке, а все результаты возвращаются в потоке пользовательского интерфейса.

0 голосов
/ 03 января 2011

Рекомендуется использовать потоки в графическом интерфейсе, а затем обновить элементы управления с помощью Control.Invoke().

Если вы не хотите использовать потоки в приложении с графическим интерфейсом, выможно использовать класс BackgroundWorker .

Лучше всего иметь в своих формах некоторую логику для обновления элементов управления извне, обычно это публичный метод.Когда этот вызов сделан из потока, который не является MainThread, вы должны защитить недопустимые обращения к потоку, используя control.InvokeRequired/control.Invoke() (где элемент управления является целевым элементом управления для обновления).

Посмотрите на этот AsynCalculatePi пример, может быть, это хорошая отправная точка.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...