Помощь - Поиск - Пользователи - Календарь
Полная версия этой страницы: Рисование из потока
Форум разработчиков электроники ELECTRONIX.ru > Сайт и форум > В помощь начинающему > Программирование
hadrov
Есть устройство, обменивающееся с компьютером по UART. Протокол вида: компьютер запросил - прибор померил и отдал результат. Для компьютера написана программа на Дельфи. Там в потоке принимается результат, и по факту приема выводится на экран в виде графика с помощью компонента TChart. Программа иногда зависает, иногда бывают аксес виалейшен.

Я хочу переписать программу на С, компилятор возьму MSVS C++.
Как следует организовывать работу с графикой и ком-портом? Мне видится, что нужно по событию от таймера проверять состояние флага приема информации, и тогда принимающий поток будет передавать точки графика тому, кто будет рисовать на форме...
Как я понял, программа часто зависала из-за того, что графика была "тяжелой", и пока что-то рисовалось в поток успевало придти несколько значений и данные терялись... Возможно, там дело обстояло несколько иначе, но мне кажется, что рисовать прямо из потока не очень хорошо.
Интересно было бы увидеть как поступают другие в подобных случаях.
И еще вопрос по графике. Есть ли что-то типа TChart в C++, уж очень там удобное масштабирование.
Читал, что рисование в DC медленное, но может кто-то пробовал его? По сути мне нужно несколько кривых выводить, правда шкала времени (ось Х) довольно длинная, около суток и более, т.е. ничего хитрого. Но с DC придется отказаться от удобностей типа масштабирования.
DpInRock
В Си ошибок только прибавится. Причем, на порядок.
А всего делов - запихивать данные из УАРТа в кольцевой буфер. А рисующий поток из этого кольцевого буфера изымает по мере возможности. И никто не страдает. В смысле - не висит.
SysRq
В Borland C++ Builder есть TChart.

Еще вариант видел: в дополнительном потоке идет работа с com-портом, а приложению постится (WM_USER + x) сообщение с принятыми данными (при этом выделялась память, передавался указатель на буфер с данными; обработчик сообщения данные обрабатывал и память обязательно освобождал).

Стандартный драйвер Windows умеет организовывать софтверные буферы большого объема на отправку\прием. Этого обычно хвататет чтобы ничего не терять.
hadrov
Цитата
В Си ошибок только прибавится. Причем, на порядок.

Программу хотят еще и на КПК перенести для работы в полевых условиях. Это одна из причин перехода на Си.

Цитата
А всего делов - запихивать данные из УАРТа в кольцевой буфер. А рисующий поток из этого кольцевого буфера изымает по мере возможности. И никто не страдает.

Надо было мне сразу кусочек кода дать.
Вот как у меня сейчас сделано:

Поток приема из УАРТа.
Код
    data := buf;
    SetEvent(Event_new_data);

В переменной buf (dword) содержится данные из УАРТа, которые мы копируем в промежуточный буфер.


Поток обработки информации.
Код
    WaitForSingleObject(MainForm.Event_new_data, INFINITE);
    ResetEvent(MainForm.Event_new_k);

    Array_data[counter] := MainForm.data;
    inc(counter);
    SetLength(Array_data, counter + 1);

    // далее сама обработка

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


Цитата
В Borland C++ Builder есть TChart.
Был бы Билдер для КПК - не знал бы я проблем.

Цитата
Еще вариант видел: в дополнительном потоке идет работа с com-портом, а приложению постится (WM_USER + x) сообщение с принятыми данными (при этом выделялась память, передавался указатель на буфер с данными; обработчик сообщения данные обрабатывал и память обязательно освобождал).
Попробую разобраться с сообщениями в Windows.

Цитата
Стандартный драйвер Windows умеет организовывать софтверные буферы большого объема на отправку\прием. Этого обычно хвататет чтобы ничего не терять.
Забыл вчера написать, что чаще всего виснет сама графика, а обработка данных происходит нормально. Рисование происходит простым добавлением новой точки, например, MainForm.Series1.AddY(high_border[i]). И зависнуть это дело может как на 10 точке добавление, так и на 10000.
Demeny
Цитата(hadrov @ Nov 11 2008, 17:18) *
Как следует организовывать работу с графикой и ком-портом? Мне видится, что нужно по событию от таймера проверять состояние флага приема информации, и тогда принимающий поток будет передавать точки графика тому, кто будет рисовать на форме...

ИМХО, таймер здесь лишний. Также мне не нравится идея вывести рисование в отдельный поток для данной задачи. В потоки удобно и нужно разносить вычисления, работающие по отношению друг к другу асинхронно и независимо, либо обслуживающие внешние воздействия, иначе Вы намучаетесь с их синхронизацией, отлавливать ситуации, когда один поток уже удалил объект, а второй осуществляет к нему доступ (access violation) и т. п.
Здесь ситуация проста - потоку рисования нечего делать, пока нет новых данных с UART, значит они должны работать синхронно и последовательно - "пришли данные --> надо рисовать", тем более, что рисование происходит заведомо быстрее, чем приходят данные с порта. Поэтому не надо усложнять - я не вижу ничего зазорного в том, чтобы рисовать прямо из потока приёма информации. Для этого удобно хранить в классе, который работает с UART, указатель на объект класса, который всё это рисует, чтобы по факту прихода данных вызвать нужный метод рисования.
SysRq
Может стоит перейти на .NET? Оно относительно одинаково заводится на всех Win* платформах.
hadrov
С учетом того, что потребуется версия для КПК, то, как я понимаю, без .NET не обойтись.
XVR
Цитата(hadrov @ Nov 12 2008, 11:37) *
Программу хотят еще и на КПК перенести для работы в полевых условиях. Это одна из причин перехода на Си.


Надо было мне сразу кусочек кода дать.
Вот как у меня сейчас сделано:

Поток приема из УАРТа.
Код
    data := buf;
    SetEvent(Event_new_data);

В переменной buf (dword) содержится данные из УАРТа, которые мы копируем в промежуточный буфер.


Поток обработки информации.
Код
    WaitForSingleObject(MainForm.Event_new_data, INFINITE);
    ResetEvent(MainForm.Event_new_k);

    Array_data[counter] := MainForm.data;
    inc(counter);
    SetLength(Array_data, counter + 1);

    // далее сама обработка

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

Во вторых, ручной сброс event'а (ResetEvent) может привести к потере одного события - нужно пользовать event с автоматическим сбросом

В третих, если 'поток обработки информации' это действительно отдельный поток, и он напрямую добавляет точки в TChart, то это и является источником зависаний - с объектами VCL можно работать ТОЛЬКО из главного потока приложения

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


Цитата
ручной сброс event'а (ResetEvent) может привести к потере одного события - нужно пользовать event с автоматическим сбросом
Это Вы за PulseEvent()? Делал и с ее помощью, но почему то автоматический сброс не происходил, вот я и сделал ручной. Попробую еще поковырять...


Цитата
если 'поток обработки информации' это действительно отдельный поток
Угу, данные обрабатываются в отдельном потоке.


Цитата
он напрямую добавляет точки в TChart, то это и является источником зависаний - с объектами VCL можно работать ТОЛЬКО из главного потока приложения
Вот где бы почитать о таких фичах, а то подобные вещи мелькают либо в FAQ'ах или приходят интуитивно, что так делать не стоит.
Тогда вопрос, каким образом TChart'у узнать, что я хочу на нем что-то нарисовать. Правильно ли будет ему ждать события от потока обработки данных WaitForSingleObject()?


Цитата
занесение и чтение данных из буфера, разделяемого между 2мя потоками, должно быть синхронизированно (например, критической секцией)
Дык вроде и синхронизировался с помощью event'ов.
Попробую с помощью CriticalSection.
Kopa
Цитата(hadrov @ Nov 12 2008, 13:46) *
С учетом того, что потребуется версия для КПК, то, как я понимаю, без .NET не обойтись.


А почему не Java, или Forth ( Форт ), Tcl/Tk ... ?
hadrov
С Си знаком лучше всего.
И я не знаю как быстро работают интерпретаторы Tcl/Tk или VM Java на КПК... имхо, лучше уж Python прикрутить.
Форт я весьма давно ковырял.
Для начала хотя бы на С# что-то написать.

Цитата
Был бы Билдер для КПК - не знал бы я проблем.

Хм, плохо я искал.
Нашел, что Borland Development Studio может под КПК делать программы.
Скачаю BDS2009 и попробую ее в первую очередь.
OlegH
Цитата(hadrov @ Nov 11 2008, 16:18) *
Есть устройство, обменивающееся с компьютером по UART. Протокол вида: компьютер запросил - прибор померил и отдал результат. Для компьютера написана программа на Дельфи. Там в потоке принимается результат, и по факту приема выводится на экран в виде графика с помощью компонента TChart. Программа иногда зависает, иногда бывают аксес виалейшен.
....
Как я понял, программа часто зависала из-за того, что графика была "тяжелой", и пока что-то рисовалось в поток успевало придти несколько значений и данные терялись... Возможно, там дело обстояло несколько иначе, но мне кажется, что рисовать прямо из потока не очень хорошо.
Интересно было бы увидеть как поступают другие в подобных случаях.
....


То ли я чего-то не понял.... То ли объяснения туманные.... Но это же весьма известный факт - из дополнительных потоков процесса ЗАПРЕЩЕНО непосредственно обращаться к экранным отображаемым объектам (компонентам-наследникам TComponent). Любые обращения к таким объектам обязаны происходить в констексте основного потока приложения или хотя бы синхронизировано с ним (большинство методов почти всех компонентов, кроме специально предназначенных, не являются thread-safe. Конфликт возникает при доступе/изменении одних и тех же данных из разных потоков).
Для более-менее удобного избегания конфликтов (если все-таки нужно из потока обращаться к компонентам) придуман метод Synchronize......
XVR
Цитата(hadrov @ Nov 12 2008, 14:55) *
Вы имеете ввиду сделать буфер типа FIFO?
Да


Цитата
Это Вы за PulseEvent()? Делал и с ее помощью, но почему то автоматический сброс не происходил, вот я и сделал ручной. Попробую еще поковырять...
Нет, это я за 2й параметр в функции CreateEvent (ManualReset), он должен быть FALSE

Цитата
Тогда вопрос, каким образом TChart'у узнать, что я хочу на нем что-то нарисовать. Правильно ли будет ему ждать события от потока обработки данных WaitForSingleObject()?
Воспользоваться synchronized процедурой

Цитата
Дык вроде и синхронизировался с помощью event'ов.
Попробую с помощью CriticalSection.
Нужно и то и другое - с помощью event'ов сообщать что появились новые данные, с помощью CriticalSection защищать доступ к этим данным (FIFO) из 2х разных потоков
CSB
...
hadrov
Цитата
Воспользоваться synchronized процедурой

Ага, теперь понял откуда ноги ростут. Эта процедура из TThread. При создании модуля с классом TThread и написано, что нельзя рисовать из потока, кроме как используя метод synchronize. Позже попробую написать его потомок, а пока я потоки реализованы с помощью API. В этом случае есть возможность рисовать из потока в главном окне?

Цитата
Нужно и то и другое - с помощью event'ов сообщать что появились новые данные, с помощью CriticalSection защищать доступ к этим данным (FIFO) из 2х разных потоков
Что бы не действовать методом научного тыка приведу краткий алгоритм, а то не уверен, что правильно Вас понял.

Поток приема из УАРТа.
Код
1. Ждем стартовый байт. (WaitCommEvent(), WaitForSingleObject())
2. Входим в критическую секцию. (EnterCriticalSection())
3. Записываем данные в буфер. (ReadFile())
4. Выходим из критической секции. (LeaveCriticalSection())
5. Устанавливаем событие в сигнальное состояние (событие с автосбросом) (SetEvent())
6. Переходим к п. 1.


Поток обработки данных
Код
1. Ждем наступления события. (WaitForSingleObject())
2. Входим в критическую секцию.
3. Извлекаем данные из буфера.
5. Выходим из критической секции.
6. Переходим к п. 1.


И как после извлечении данных из буфера и выхода из критической секции вставить пункт "Рисуем в главном окне", чтобы он отработал нормально?
XVR
Цитата(hadrov @ Nov 18 2008, 02:14) *
Ага, теперь понял откуда ноги ростут. Эта процедура из TThread. При создании модуля с классом TThread и написано, что нельзя рисовать из потока, кроме как используя метод synchronize. Позже попробую написать его потомок, а пока я потоки реализованы с помощью API. В этом случае есть возможность рисовать из потока в главном окне?

Что бы не действовать методом научного тыка приведу краткий алгоритм, а то не уверен, что правильно Вас понял.

Поток приема из УАРТа.
Код
1. Ждем стартовый байт. (WaitCommEvent(), WaitForSingleObject())
2. Входим в критическую секцию. (EnterCriticalSection())
3. Записываем данные в буфер. (ReadFile())
4. Выходим из критической секции. (LeaveCriticalSection())
5. Устанавливаем событие в сигнальное состояние (событие с автосбросом) (SetEvent())
6. Переходим к п. 1.


Поток обработки данных
Код
1. Ждем наступления события. (WaitForSingleObject())
2. Входим в критическую секцию.
3. Извлекаем данные из буфера.
5. Выходим из критической секции.
6. Переходим к п. 1.
Да, именно так



Цитата
И как после извлечении данных из буфера и выхода из критической секции вставить пункт "Рисуем в главном окне", чтобы он отработал нормально?
Посадить потоки на TThread, или вручную реализовать то, что сделано в TThread::Synchorize. А именно -
  1. Данные помещаются в очередь (можно просто переменная)
  2. Главному окну посылается специальное сообщение (любое)
  3. WaitForSingleObject(sync_event)
В обработчике специального сообщения главного окна:
  1. Забираем данные, рисуем
  2. SetEvent(sync_event)
Event sync_event должен быть создан как

Код
  sync_event=CreateEvent(NULL,FALSE,FALSE,NULL);
AHTOXA
Цитата(hadrov @ Nov 18 2008, 04:14) *
Поток приема из УАРТа.
...


Не так.
Поток приема из УАРТа.
  1. Ждем стартовый байт. (WaitCommEvent(), WaitForSingleObject())
  2. Входим в критическую секцию. (EnterCriticalSection()) - не надо, больше никто всё равно не читает из порта.
  3. Записываем данные в буфер. (ReadFile())
  4. Выходим из критической секции. (LeaveCriticalSection())
  5. Распределяем память под принятое количество байтов и копируем туда принятое;
  6. Отправляем указатель на этот блок памяти в главное окно при помощи PostMessage(WM_USER+3);
  7. Устанавливаем событие в сигнальное состояние (событие с автосбросом) (SetEvent()) - это тоже лишнее, поскольку мы отправили сообщение сразу в главное окно и никто не спит, дожидаясь данных.
  8. Переходим к п. 1.

Основной поток программы:
  1. Ждем сообщения WM_USER+3;
  2. Берём данные из указателя - параметра, отрисовываем их.
  3. Уничтожаем память под указателем.
  4. Переходим к п. 1.
XVR
Цитата(AHTOXA @ Nov 18 2008, 10:40) *
Не так.
Поток приема из УАРТа.
  1. Ждем стартовый байт. (WaitCommEvent(), WaitForSingleObject())
  2. Входим в критическую секцию. (EnterCriticalSection()) - не надо, больше никто всё равно не читает из порта.
  3. Записываем данные в буфер. (ReadFile())
  4. Выходим из критической секции. (LeaveCriticalSection())
  5. Распределяем память под принятое количество байтов и копируем туда принятое;
  6. Отправляем указатель на этот блок памяти в главное окно при помощи PostMessage(WM_USER+3);
  7. Устанавливаем событие в сигнальное состояние (событие с автосбросом) (SetEvent()) - это тоже лишнее, поскольку мы отправили сообщение сразу в главное окно и никто не спит, дожидаясь данных.
  8. Переходим к п. 1.
Основной поток программы:
  1. Ждем сообщения WM_USER+3;
  2. Берём данные из указателя - параметра, отрисовываем их.
  3. Уничтожаем память под указателем.
  4. Переходим к п. 1.
Частые заказы/освобождения блоков памяти череваты возростанием фрагментации кучи, что может привести к перерасходу памяти и общим тормозам для всей программы. Все таки рекомендуется сделать кольцевой буффер.

Да, и делать 2 дополнительных потока это тоже перебор, вполне хватит и одного - для приема. Обработку и отрисовку можно делать в главном потоке приложения.
AHTOXA
Цитата(XVR @ Nov 18 2008, 14:25) *
Частые заказы/освобождения блоков памяти череваты возростанием фрагментации кучи, что может привести к перерасходу памяти и общим тормозам для всей программы. Все таки рекомендуется сделать кольцевой буффер.


Ерунда. Не те объёмы. Да и блоки скорее всего фиксированной длины.

Цитата(XVR @ Nov 18 2008, 14:25) *
Да, и делать 2 дополнительных потока это тоже перебор, вполне хватит и одного - для приема. Обработку и отрисовку можно делать в главном потоке приложения.


Ну а я как написал?
hadrov
Цитата(AHTOXA @ Nov 18 2008, 13:46) *
Ну а я как написал?

Скорее всего, это адресовалось мне.


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

Т.к. следующее предположение верно
Цитата
Да и блоки скорее всего фиксированной длины.
, то можно ведь изменить пункт
Цитата
# Распределяем память под принятое количество байтов и копируем туда принятое;

на "один раз выделили в начале и потом просто пользуемся этим указателем до конца работы".

Попутно вопрос: сейчас данные приходят через фиксированные промежутки времени, данные успевают обработаться за это время, и поэтому, как я понимаю, хватает одной ячейки во втором буфере. А теперь представим ситуацию, когда данные приходят нерегулярно. Тут уже потребуется FIFO с достаточным количеством ячеек?
AHTOXA
Цитата(hadrov @ Nov 18 2008, 15:32) *
Я сейчас сделал еще один тестовый вариант с использованием компоненты TComPort. Там нечто подобное внутри реализовано - крутится поток и при приеме данных возникает событие, данные сохраняются в буфере.


Да, это как раз очень подходящий пример.

Цитата
Т.к. следующее предположение верно, то можно ведь изменить пункт "Распределяем память под принятое количество байтов и копируем туда принятое;"
на "один раз выделили в начале и потом просто пользуемся этим указателем до конца работы".


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

Цитата
Попутно вопрос: сейчас данные приходят через фиксированные промежутки времени, данные успевают обработаться за это время, и поэтому, как я понимаю, хватает одной ячейки во втором буфере. А теперь представим ситуацию, когда данные приходят нерегулярно. Тут уже потребуется FIFO с достаточным количеством ячеек?


В винде никогда нельзя закладываться на то, что программа успеетsmile.gif
hadrov
Цитата
Нет, так нельзя.
Теперь ясно. Я не так понял "Распределяем память под принятое количество байтов и копируем туда принятое". Почему то подумал, что это всегда будет одна и та область памяти. А тут мы каждый раз выделяем новую, но при этом указатели на старые куски данных могут быть еще валидными. Ну, и таким образом отпал вопрос о не успевании программы.
defunct
Цитата(hadrov @ Nov 12 2008, 13:55) *
Вот где бы почитать о таких фичах, а то подобные вещи мелькают либо в FAQ'ах или приходят интуитивно, что так делать не стоит.

На форуме мастеров делфи. http://www.delphimaster.ru/
Для просмотра полной версии этой страницы, пожалуйста, пройдите по ссылке.
Invision Power Board © 2001-2025 Invision Power Services, Inc.