DispatcherTimer и пользовательский интерфейс обновляют ограничения в C# silverlight

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

У меня есть картографическое приложение (Visiblox), которое я использую в качестве скользящей области, обновляемой каждые 20 мс, добавляя и удаляя точку. В псевдокоде:

List<Point> datapoints= new List<Point>();
Series series = new Series(datapoints);
void timer_tick(){
  datapoints.Add(new Point);
  datapoints.RemoveAt(0);
  // no need to refresh chart, it does refresh automatically
}

При запуске 6 серий в этом инструменте построения графиков он начал показывать себя немного вяло. Изменение отметки на 10 мс не имело никакого значения, график обновлялся с той же скоростью, поэтому кажется, что 20 мс - это ограничение скорости (пользовательский интерфейс или график?).

Я пробовал с CompositionTarget.Rendering и получил те же результаты: ниже 20 мс не было никакой разницы в скорости.

Тогда я случайно включил оба, и скорость удвоилась. Поэтому я протестировал несколько потоков (2, 3, 4) и скорость удвоилась, утроилась и увеличилась в четыре раза. Это еще не имеет блокировок, так как я даже не знаю, какой процесс мне нужен, чтобы генерировать блокировку, но не было никаких повреждений данных или утечек памяти.

У меня такой вопрос: почему вялый график на 20 мс не может работать на 10 мс, но смехотворно быстр в многопоточном режиме? Процесс обновления пользовательского интерфейса выполняется быстрее? Вычисляется ли вдвое больше карт? Или существует ли ограничение на скорость выполнения одного DispatcherTimer?

Спасибо!


Изменить: у меня есть опыт встроенного кодирования, поэтому, когда я думаю о потоках и таймингах, я сразу же думаю о переключении булавки в аппаратном обеспечении и подключаю область для измерения длины процесса. Я новичок в потоках в C# и нет контактов для подключения областей. Есть ли способ увидеть время потоков графически?

2 ответа

Решение

Ключевым моментом здесь, я думаю, является осознание того, что Silverlight рендерится с максимальной частотой кадров 60 кадров в секунду по умолчанию (настраивается через свойство MaxFrameRate). Это означает, что тики DispatcherTimer будут срабатывать не более 60 раз в секунду. Кроме того, вся работа рендеринга происходит также в потоке пользовательского интерфейса, поэтому DispatcherTimer запускается с той скоростью, с которой в лучшем случае происходит рисование, как указывалось в предыдущем постере.

Результатом того, что вы делаете, добавляя три таймера, является просто запуск метода "добавить данные" 3 раза за цикл обработки событий, а не один раз, поэтому будет выглядеть, как будто ваши диаграммы идут намного быстрее, но на самом деле частота кадров примерно тот же самый. Вы можете получить тот же эффект с одним DispatcherTimer и просто добавить в 3 раза больше данных на каждый тик. В этом можно убедиться, подключившись к событию CompositionTarget.Rendering и параллельно подсчитав частоту кадров.

Точка ObservableCollection, сделанная ранее, является хорошей, но в Visiblox есть немного магии, чтобы попытаться смягчить последствия этого, поэтому, если вы добавляете данные с очень высокой скоростью, обновления диаграммы будут сгруппированы со скоростью цикл рендеринга и ненужные повторные рендеры будут отброшены.

Кроме того, что касается вашего мнения о том, что вы привязаны к реализации ObservableCollection IDataSeries, вы полностью можете самостоятельно реализовать интерфейс IDataSeries, например, поддерживая его простым List. Просто имейте в виду, что, очевидно, если вы сделаете это, диаграмма больше не будет автоматически обновляться при изменении данных. Вы можете принудительно обновить диаграмму, вызвав Chart.Invalidate() или изменив вручную заданный диапазон осей.

DispatcherTimer, который запускает событие Tick в потоке пользовательского интерфейса, - это то, что считается таймером с низким разрешением или с низкой точностью, поскольку его интервал фактически означает "тик не раньше, чем x с момента последнего тика". Если поток пользовательского интерфейса занят чем-либо (обработкой ввода, обновлением графика и т. Д.), Он будет задерживать события таймера. Кроме того, наличие большого числа элементов DispatcherTimer, тикающих в потоке пользовательского интерфейса с очень малыми интервалами, также замедлит скорость отклика вашего приложения, поскольку во время вызова события Tick приложение не может отвечать на входные данные.

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

В вашем примере у вас есть комментарий, который гласит: "Нет необходимости обновлять диаграмму, она обновляется автоматически". Это заставляет меня задаться вопросом, как график узнает, что вы изменили datapoints коллекция? List<T> не вызывает события при его изменении. Если бы вы использовали ObservableCollection<T> Я хотел бы отметить, что каждый раз, когда вы удаляете / добавляете точку, вы потенциально обновляете график, что может замедлять процесс.

Но если вы на самом деле используете List<T> тогда должно быть что-то еще (возможно, другой таймер?), который обновляет график. Может быть, сам элемент управления диаграммы имеет встроенный механизм автообновления?

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

Одним из способов минимизировать это было бы использовать LinkedList<T> вместо List<T>, К концу LinkedList добавляется O(1), поэтому удаляется элемент. List<T> необходимо удалить все на единицу, когда вы удаляете элемент с самого начала. Используя LinkedList, вы можете заблокировать его в фоновом потоке (ах) и минимизировать время удержания блокировки. В потоке пользовательского интерфейса вам также необходимо получить такую ​​же блокировку и либо скопировать список в массив, либо обновить диаграмму, пока блокировка удерживается.

Другим возможным решением было бы буферизовать "порции" точек в фоновом потоке и опубликовать пакет из них в потоке пользовательского интерфейса с помощью Dispatcher.BeginInvoke, где вы могли бы затем безопасно обновить коллекцию.

Другие вопросы по тегам