Предотвращение зависания пользовательского интерфейса без дополнительных потоков - PullRequest
6 голосов
/ 29 октября 2009

Какие решения у меня есть, если я хочу предотвратить зависание пользовательского интерфейса при десериализации большого количества элементов пользовательского интерфейса в WPF? Я получаю сообщения об ошибках, что объекты принадлежат потоку пользовательского интерфейса, когда я пытаюсь загрузить их в другой поток. Итак, какие варианты я должен предотвратить ошибку «Программа не отвечает» во время загрузки данных пользовательского интерфейса? Могу ли я рассчитывать на однопоточное решение или я что-то упускаю из-за нескольких потоков пользовательского интерфейса?

Ответы [ 7 ]

12 голосов
/ 29 октября 2009

Если вы используете только один поток, пользовательский интерфейс будет зависать, пока вы выполняете любую обработку.

Если вы используете поток BackgroundWorker, у вас будет больше контроля над тем, что и когда происходит.

Чтобы обновить пользовательский интерфейс, вам нужно использовать Dispatcher.Invoke из фонового потока для перенаправления вызова через границу потока.

Dispatcher.Invoke(DispatcherPriority.Background,
                  new Action(() => this.TextBlock.Text = "Processing");
2 голосов
/ 30 октября 2009

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

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

Каждый раз, когда вы получаете контроль, все, что вам нужно сделать, это:

  1. Создать DispatcherFrame
  2. Поставить в очередь событие диспетчеру, используя BeginInvoke, который устанавливает Continue = false в кадре
  3. Используйте PushFrame для запуска кадра, работающего на Диспетчере

Кроме того, при вызове самого десериализатора убедитесь, что вы делаете это из Dispatcher.BeginInvoke, или что ваш код вызова не удерживает никаких блокировок и т. Д.

Вот как это будет выглядеть:

  public partial class MyWindow
  {
    SomeDeserializer _deserializer = new SomeDeserializer();

    byte[] _sourceData;
    object _deserializedObject;

    ...

    void LoadButton_Click(...)
    {
      Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
      {
        _deserializedObject = _deserializer.DeserializeObject(_sourceData);
      }));
    }
  }

  public class OneOfTheObjectsBeingDeserializedFrequently
  {
    ...

    public string SomePropertyThatIsFrequentlySet
    {
      get { ... }
      set { ...; BackgroundThreadingSolution.DoEvents(); }
    }
  }

  public class BackgroundThreadingSolution
  {
    [ThreadLocal]
    static DateTime _nextDispatchTime;

    public static void DoEvents()
    {
      // Limit dispatcher queue running to once every 200ms
      var now = DateTime.Now;
      if(now < _nextDispatchTime) return;
      _nextDispatchTime = now.AddMilliseconds(200);

      // Run the dispatcher for everything over background priority
      var frame = new DispatcherFrame();
      Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
      {
        frame.Continue = false;
      }));
      Dispatcher.PushFrame(frame);
    }
  }

Проверка DateTime.Now в DoEvents () фактически не требуется для работы этого метода, но улучшит производительность, если SomeProperty будет установлен очень часто во время десериализации.

Редактировать: Сразу после того, как я написал это, я понял, что есть более простой способ реализовать метод DoEvents. Вместо использования DispatcherFrame просто используйте Dispatcher.Invoke с пустым действием:

    public static void DoEvents()
    {
      // Limit dispatcher queue running to once every 200ms
      var now = DateTime.Now;
      if(now < _nextDispatchTime) return;
      _nextDispatchTime = now.AddMilliseconds(200);

      // Run the dispatcher for everything over background priority
      Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new Action(() => {}));
    }
2 голосов
/ 29 октября 2009

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

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

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

У меня была похожая проблема с моей панелью, которая перемещала свои элементы. Пользовательский интерфейс зависал, потому что я использовал DispatcherTimer с приоритетом Loaded. Проблема исчезла, как только я изменил ее на DispatcherPriority.Input.

0 голосов
/ 29 октября 2009

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

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

0 голосов
/ 29 октября 2009

Рекомендации из блога OldNewThing.

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

So One GUI Thread Множество рабочих потоков, выполняющих эту работу.

Если какой-либо из ваших потоков зависает, пользователь непосредственно контролирует ваше приложение, может закрыть поток, не влияя на его интерфейс приложения. Это сделает его счастливым, потому что ваш пользователь будет постоянно контролировать, кроме него, нажимая кнопку «Остановить стоп», и он не прекратит поиск.

0 голосов
/ 29 октября 2009

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

...