Каков предпочтительный способ получить код для выполнения в потоке графического интерфейса WPF? - PullRequest
2 голосов
/ 30 января 2012

У меня есть основной поток, который содержит мой WPF GUI и один или несколько фоновых потоков, которым время от времени требуется асинхронно выполнять код в основном потоке (например, обновление состояния в GUI).

Есть два способа (о которых я знаю, может быть, больше), чтобы сделать это:

  1. путем планирования задачи с использованием TaskScheduler из контекста синхронизации целевого потока и
  2. путем вызова делегата с использованием Dispatcher из целевого потока.

В коде:

using System.Threading.Tasks;
using System.Threading;

Action mainAction = () => MessageBox.Show(string.Format("Hello from thread {0}", Thread.CurrentThread.ManagedThreadId));
Action backgroundAction;

// Execute in main thread directly (for verifiying the thread ID)
mainAction();

// Execute in main thread via TaskScheduler
var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
backgroundAction = () => Task.Factory.StartNew(mainAction, CancellationToken.None, TaskCreationOptions.None, taskScheduler);
Task.Factory.StartNew(backgroundAction);

// Execute in main thread via Dispatcher
var dispatcher = System.Windows.Threading.Dispatcher.CurrentDispatcher;
backgroundAction = () => dispatcher.BeginInvoke(mainAction);
Task.Factory.StartNew(backgroundAction);

Мне нравится версия на основе TPL, потому что я уже много использую TPL, и она обеспечивает мне большую гибкость, например, ожидая выполнения задачи или связывая ее с другими задачами. Однако в большинстве примеров кода WPF, которые я видел до сих пор, использовался вариант Dispatcher.

Если я не нуждаюсь в такой гибкости и хочу только выполнить некоторый код в целевом потоке, то по каким причинам один предпочитает один путь другому? Есть ли какие-либо последствия для производительности?

Ответы [ 3 ]

9 голосов
/ 31 января 2012

Я настоятельно рекомендую вам прочитать документ Asynchronous Pattern на основе задач.Это позволит вам структурировать свои API-интерфейсы, чтобы они были готовы к выходу на улицу async и await.

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

Документ TAP имеет простое решение, которое более элегантно решает проблему: если фоновая операция хочет выдавать отчеты о ходе выполнения, она принимает аргумент типаIProgress<T>:

public interface IProgress<in T> { void Report(T value); }

В этом случае базовую реализацию относительно просто предоставить:

public sealed class EventProgress<T> : IProgress<T>
{
  private readonly SynchronizationContext syncContext;

  public EventProgress()
  {
    this.syncContext = SynchronizationContext.Current ?? new SynchronizationContext();
  }

  public event Action<T> Progress;

  void IProgress<T>.Report(T value)
  {
    this.syncContext.Post(_ =>
    {
      if (this.Progress != null)
        this.Progress(value);
    }, null);
  }
}

(SynchronizationContext.Current по сути TaskScheduler.FromCurrentSynchronizationContext без необходимости фактических Task с).

Async CTP содержит IProgress<T> и тип Progress<T>, который аналогичен EventProgress<T> выше (но более производительный).Если вы не хотите устанавливать вещи уровня CTP, то вы можете просто использовать приведенные выше типы.

Подводя итог, на самом деле есть четыре варианта:

  1. IProgress<T> -именно так в будущем будет написан асинхронный код.Это также вынуждает вас отделять фоновую логику работы от вашего кода обновления пользовательского интерфейса / ViewModel , что хорошо.
  2. TaskScheduler - неплохой подход;это то, что я использовал долгое время перед переключением на IProgress<T>.Однако он не вытесняет код обновления пользовательского интерфейса / ViewModel из логики фоновых операций.
  3. SynchronizationContext - те же преимущества и недостатки, что и у TaskScheduler, через менее известный API .
  4. Dispatcher - действительно может не рекомендовать это!Рассмотрим фоновые операции по обновлению ViewModel - так что в коде прогресса обновления нет ничего специфичного для пользовательского интерфейса.В этом случае, используя Dispatcher, просто привязали вашу ViewModel к вашей платформе пользовательского интерфейса.Nasty.

PS Если вы решите использовать Async CTP, у меня есть несколько дополнительных IProgress<T> реализаций в моей библиотеке Nito.AsyncEx , включая одну (PropertyProgress) отправляет отчеты о ходе выполнения через INotifyPropertyChanged (после переключения обратно в поток пользовательского интерфейса через SynchronizationContext).

1 голос
/ 30 января 2012

Я обычно использую Dispatcher для любых небольших операций с пользовательским интерфейсом, и использую Задачи, только если требуется много тяжелой обработки.Я также использую TPL для всех фоновых задач, не связанных с пользовательским интерфейсом.

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

0 голосов
/ 30 января 2012

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

обновление: Пит Браун хорошо освещает это в своем блоге http://10rem.net/blog/2010/04/23/essential-silverlight-and-wpf-skills-the-ui-thread-dispatchers-background-workers-and-async-network-programming

...