Как правильно работать с мультимедийным таймером?

Вопросы программирования и использования среды Lazarus.

Модератор: Модераторы

Andreich
постоялец
Сообщения: 268
Зарегистрирован: 17.04.2008 12:33:43

Как правильно работать с мультимедийным таймером?

Сообщение Andreich »

Всем доброго времени суток! Перерыл весь Интернет, но проблему так и не решил... каким образом следует реализовать мультимедийный таймер в среде Lazarus? В Delphi это делается так:

Код: Выделить всё

var
  Form1: TForm1;
  mmResult : integer;

...

procedure TimeCallBack(uTimerID, uMsg: UINT; dwUser, dw1, dw2: DWORD);stdcall;
var px1, py1, px2, py2 : integer;
begin
  px1:=random(100);
  py1:=random(100);
  px2:=random(100);
  py2:=random(100);
  Form1.Image1.Canvas.Brush.Color:=RGB(random(255),random(255),random(255));
  Form1.Image1.Canvas.Rectangle(px1,py1,px2,py2);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  mmResult := TimeSetEvent(10,0, @TimeCallBack, 0, TIME_PERIODIC);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Form1.Image1.Canvas.Rectangle(0,0,100,100);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  TimeKillEvent(mmResult);
end;

При переносе этого кода на Lazarus внешне все проходит без ошибок, но при запуске на исполнение программа самопроизвольно вырубается спустя несколько секунд (время может быть разным от 1 до 50 сек.). Проверил на Delphi - все работает идеально. В чем загвоздка?

Lazarus 0.9.30 & FPC 2.4.2
Bupyc
постоялец
Сообщения: 137
Зарегистрирован: 29.08.2007 18:22:42

Сообщение Bupyc »

А Вы уверены, что у Вас выполнение процедуры TimeCallBack происходит в контексте основного потока приложения и там вот прям так безнаказанно можно вызывать LCL код? Я бы все таки через некий аналог Synchronize из TThread вызывал тот код, который Вы сейчас напрямую вызываете.

Вызовите API GetCurrentThreadId при запуске события (на кнопке) и в процедуре TimeCallBack. Если возвращаются разные значения, то контекст потоков разный и принципиально неверно вызывать код из библиотеки LCL в обработчике таймера, причем как в Delphi, так и в Lazarus.
Andreich
постоялец
Сообщения: 268
Зарегистрирован: 17.04.2008 12:33:43

Сообщение Andreich »

Bupyc писал(а):Вызовите API GetCurrentThreadId при запуске события (на кнопке) и в процедуре TimeCallBack. Если возвращаются разные значения, то контекст потоков разный и принципиально неверно вызывать код из библиотеки LCL в обработчике таймера, причем как в Delphi, так и в Lazarus.

Значения разные. Исключил из TimeCallBack обращение к объектам LCL для проверки заменив содержимое процедуры абстрактными вычислениями,.. все работает без сбоев! Спасибо за данную подсказку, просто я раньше практически не работал с потоками.

Теперь осталось разобраться как производить отрисовку графики при срабатывании таймера (собственно для того и нужен быстрый мультимедийный таймер).
Bupyc
постоялец
Сообщения: 137
Зарегистрирован: 29.08.2007 18:22:42

Сообщение Bupyc »

Если значения аргументов процедуры TimeCallBack не используются, можно через функцию PostMessage отправлять форме сообщение, о том что сработал таймер. Пусть она по этому сообщению что-то делает. Если аргументы TimeCallBack (uTimerID, uMsg, dwUser, dw1, dw2) нужны, то можно при каждом срабатывании таймера динамически выделять в куче память под структуру и помещать значения аргументов в неё, а затем одним из аргументов сообщения (wParam или lParam) передавать полученный указатель в форму. В обработчике сообщения не забываем освобождать выделенную память.

Добавлено спустя 59 секунд:
Andreich писал(а): Спасибо за данную подсказку, просто я раньше практически не работал с потоками.


Я работаю уже лет 15 :) Всякое видел.

Добавлено спустя 33 минуты 27 секунд:
Кстати, вот еще о чем подумал. Если будете передавать аргументы процедуры TimeCallBack в основной поток приложения тем способом, который я Вам предложил, выделение памяти в куче делайте с помощью виндовых функций (например, HeapAllocate).
Поясню, почему так: не знаю как в Лазарусе, в дельфях менеджер памяти анализирует значение глобальной переменной IsMultithread при работе с динамической памятью.

Как говорит help
IsMultiThread is set to true by BeginThread and class factories


Таким образом, если Вы не создадите никаких потоков с помощью BeginThread или наследника от класса TThread, то переменная IsMultiThread останется равной False. Хотя де-факто приложение будет мультипоточным. Если при этом вызывать в TimeCallBack код, который будет обращаться к менеджеру памяти (функции GetMem, FreeMem, Move, FillChar, работа со строками, динамическими массивами и т.д.), то поведение программы может стать непредсказуемым.

В общем, совет - наполнение TimeCallBack сделайте минимальным. Иначе можно наглядеться магических глюков в произвольном месте программы.
zub
долгожитель
Сообщения: 2890
Зарегистрирован: 14.11.2005 22:51:26
Контактная информация:

Сообщение zub »

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

по тойже причине что и вызывать LCLный код не стоит. Лучше заполнять заранее выделенный (или объявленный) в основной программе кусок памяти
Самое простое - внутри TimeCallBack просто устанавливать флажок, по установке флажка отрисовывать в onIdle и снова его сбрасывать
Bupyc
постоялец
Сообщения: 137
Зарегистрирован: 29.08.2007 18:22:42

Сообщение Bupyc »

zub писал(а):Выделять память в TimeCallBack, освобождать внутри обработчиков - верный путь к
Иначе можно наглядеться магических глюков в произвольном месте программы.


Утечку памяти можно получить, особенно при закрытии программы. Насчет глюков - не уверен. Довольно часто так делаю. Причем в коде, работающем при высокой нагрузке по нескольку месяцев. Проблем не наблюдал. Может везло? :)

zub писал(а):Самое простое - внутри TimeCallBack просто устанавливать флажок, по установке флажка отрисовывать в onIdle и снова его сбрасывать


Может получиться так, что TimeCallBack вызовется 10 раз, а OnIdle - 1. Если такой нюанс не критичен, то этот способ подходит.
zub
долгожитель
Сообщения: 2890
Зарегистрирован: 14.11.2005 22:51:26
Контактная информация:

Сообщение zub »

>>Может получиться так, что TimeCallBack вызовется 10 раз, а OnIdle - 1. Если такой нюанс не критичен, то этот способ подходит.
в лцлприложении по любому так и получится, вернее нет никаких гарантий что так не получится, как не ухитряйся возвращать данные\вызывать методы из мультимедийного таймера. ИМХО.

>>Может везло?
Может, мне один раз (давным давно, вроде еще в Делфи) не повезло, с тех пор ни-ни :lol: Возможно сейчас такие выделения не страшны
Аватара пользователя
B4rr4cuda
энтузиаст
Сообщения: 693
Зарегистрирован: 28.12.2007 06:48:35

Сообщение B4rr4cuda »

Bupyc писал(а):Если значения аргументов процедуры TimeCallBack не используются, можно через функцию PostMessage отправлять форме сообщение, о том что сработал таймер. Пусть она по этому сообщению что-то делает. Если аргументы TimeCallBack (uTimerID, uMsg, dwUser, dw1, dw2) нужны, то можно при каждом срабатывании таймера динамически выделять в куче память под структуру и помещать значения аргументов в неё, а затем одним из аргументов сообщения (wParam или lParam) передавать полученный указатель в форму. В обработчике сообщения не забываем освобождать выделенную памят

По рукам бить надо за такие советы написания кода на лазарусе и фрипаскале =))
В таком случае лучше уж кроссплатформенно делать передачу событий, через IPC (модуль simpleipc, классы TSimpleIPCServer и TSimpleIPCClient).
Bupyc
постоялец
Сообщения: 137
Зарегистрирован: 29.08.2007 18:22:42

Сообщение Bupyc »

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

Так что совет вполне нормальный. Хотя, согласен, далеко не самое красивое решение. Можно сделать лучше. Особенно если предполагается, что код будет работать где то еще кроме Windows. Но в этом случае и мультимедийными таймерами лучше не пользоваться.
Andreich
постоялец
Сообщения: 268
Зарегистрирован: 17.04.2008 12:33:43

Сообщение Andreich »

Brainenjii писал(а):Есть ещё такая штука - http://wiki.lazarus.freepascal.org/EpikTimer

Да, я ставил этот компонент, но так толком и не разобрался, как привязать к срабатыванию какое-то действие... там нет события onTimer. В демонстрационном примере акцент сделан на точное измерение временных интервалов, а не на обработку события по таймеру.
alexey38
долгожитель
Сообщения: 1627
Зарегистрирован: 27.04.2011 19:42:31

Сообщение alexey38 »

Andreich писал(а):
Brainenjii писал(а):Есть ещё такая штука - http://wiki.lazarus.freepascal.org/EpikTimer

Да, я ставил этот компонент, но так толком и не разобрался, как привязать к срабатыванию какое-то действие... там нет события onTimer. В демонстрационном примере акцент сделан на точное измерение временных интервалов, а не на обработку события по таймеру.


Если просто нужно замерять временные интервалы под виндой, то используйте: GetTickCount - выдает в мсек. А лучше GetSystemTimeAsFileTime - там вообще с более чем 1 мксек точностю идет замер и нет переполнения, как в GetTickCount.
Andreich
постоялец
Сообщения: 268
Зарегистрирован: 17.04.2008 12:33:43

Сообщение Andreich »

alexey38 писал(а):Если просто нужно замерять временные интервалы под виндой, то используйте: GetTickCount - выдает в мсек. А лучше GetSystemTimeAsFileTime - там вообще с более чем 1 мксек точностю идет замер и нет переполнения, как в GetTickCount.

В том то все и дело, что мне не нужно замерять интервалы, а нужно выполнять некоторый набор действий по срабатыванию таймера.
alexey38
долгожитель
Сообщения: 1627
Зарегистрирован: 27.04.2011 19:42:31

Сообщение alexey38 »

Andreich писал(а):
alexey38 писал(а):Если просто нужно замерять временные интервалы под виндой, то используйте: GetTickCount - выдает в мсек. А лучше GetSystemTimeAsFileTime - там вообще с более чем 1 мксек точностю идет замер и нет переполнения, как в GetTickCount.

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


Системный таймер (SetTimer): Если обработчик таймера не успевает закончить все действия в установленный интервал, то последующие вызовы этого обработчика становятся в очередь. Это приводит к тому, что на разных компьютерах приложение работает с разной скоростью. Сама же CallBack функция вызывается в контексте основного потока. Мультимедийный таймер (timeSetEvent): Если обработчик таймера не успевает закончить все действия в установленный интервал, то последующие вызовы накапливаться не будут. Сама же CallBack функция вызывается в контексте отдельного потока.

Так что м.б. нужен системный таймер, а не мультимедийный. Он собственно мультимедийный, чтобы работать с какими-то устройствами, а не для вывода на формы. А если нужен мультимедийный, то можно не call-back использовать, а устанавливать Event. Так как работа с многопоточными приложениями требует использование всяких синхронизаций, типа критических секций и т.п., иначе будут разные глюки.
Аватара пользователя
B4rr4cuda
энтузиаст
Сообщения: 693
Зарегистрирован: 28.12.2007 06:48:35

Сообщение B4rr4cuda »

Bupyc вопрос не в мультимедийном таймере, а в методике "общения" с потоками) А именно в массовом использовании PostMessage и SendMessage с AllocateWnd для создания моста между потоками. Это весьма удобно и хорошо использовать в делфи, но на лазаре - это проблема именно ввиду его кроссплатформенности. Данная методика очень "привязывается" и используется везде, где надо и не надо, что потом создает серьезные проблемы. Столкнулся лично, поэтому вопрос "больной".. :) Прошу прощения, если задел чувства или оскорбил.
Ответить