6

Всем привет! Может кто-то пояснить механизм работы таймера qt. Он формирует сообщение в очереди сообщений или влияет на процесс обработки этой очереди?

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

Теперь если в том же потоке есть таймер. Он будет параллельно со слотом класть в очередь сообщения о таймауте, как бы из другого потока? Сможет ли он прервать мой вечный цикл указанный выше? Или он просто ставит где-то в обработчики очереди сообщений флажочек и в нужное время вызывается сигнал таймера?

UPD: Спасибо за оба ответа, отдал предпочтение второму ответу, так как он все же более полный и ближе, как мне показалось к тому что спрашивал.

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

  • Небольшая поправка к резюме: любой сигнал, кроме QObject::destroyed(), может быть заблокирован при помощи QObject::blockSignals(). С таймером получится так, что к нему событие о таймауте от системы придёт, а он свой сигнал не создаст и не отправит. –  Jun 20 '16 at 13:35

2 Answers2

4

TL; DR - Да, таймер сможет вклиниться в косвенную рекурсию, но так делать не стоит

Для создания "вечных циклов" в обработке сообщений есть специальный механизм:

As a special case, a QTimer with a timeout of 0 will time out as soon as all the events in the window system's event queue have been processed. This can be used to do heavy work while providing a snappy user interface:

Особый случай - QTimer с таймаутом 0, который срабатывает как только очередь сообщений оказывается пустой. Это позволяет выполнять вычисления и одновременно показывать пользователю отзывчивый интерфейс.

Это косвенно говорит о том, что таймер работает как описанный вами "флажочек", и проверка на сработку таймера происходит всякий раз, когда программа оказывается в очереди сообщений.

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

Источник - мануал

gbg
  • 22,253
  • Если есть функция которая вырабатывает 3 сигнала (0, 1, 2) и они попадают в очередь сообщений. И параллельно работает таймер с той же очередью. Можно утверждать что таймер будет всегда обрабатываться только между 2 и 0 или он может вклиниться куда угодно между 0, 1, 2? – Andrey Golikov Jun 20 '16 at 07:45
  • 1
    @Andrey-Golicov - однозначно, ответ надо искать в документации. Если нигде это не задокументировано, полагаться на последовательное помещение сигналов в очередь нельзя – gbg Jun 20 '16 at 08:32
  • Не нашел я такого в документации. Тут нужен именно механизм, когда возникает сигнал таймера. Если во время возврата к очереди, то подключив таймер не на прямую, а очередью, я могу утверждать что сообщение о таймауте ляжет в конец списка сигналов. и 0,1,2 никогда не будет разорвано таймером. Если сигнал рождается асинхронно, то он может родиться и пока я в функции и ляжет куда угодно. – Andrey Golikov Jun 20 '16 at 11:33
  • 1
    @AndreyGolikov коль скоро в документации этого нет - принимайте пессимистичную точку зрения и не полагайтесь на определенный порядок работы. – gbg Jun 20 '16 at 11:47
  • 1
    @AndreyGolikov , событие о таймауте не окажется между событиями "0", "1" и "2" только в том случае, если между отправками этих событий Вы сможете гарантировать, что там не окажется какой-нибудь вызов, например QCoreApplication::processEvents(). А это запросто может случиться по причине невнимательности и/или объёмности кода. Иными словами, рекомендация gbg принимать пессимистичную точку зрения более чем обоснована. –  Jun 20 '16 at 11:59
  • Трудно достоверно считать что этого нет в документации, может я просто не нашел:). Просто для двойной проверки, в QCoreApplication::processEvents() разбирается очередь текущих сообщений потока, а так же обрабатываются все накопившиеся с прошлого вызова сообщения операционки, среди которых и таймаут таймера? Поэтому этот сигнал будет добавлен в очередь как раз во время вызова, мы говорим именно о факте когда сигнал рождается, а не начинает обрабатываться, правильно? – Andrey Golikov Jun 20 '16 at 12:51
  • @AndreyGolikov , мне лично тоже не встречалось, но это просто для Вас этот момент оказался принципиален. Обычно используют собственные флаги или машину состояний для того, чтобы исключить ненужную непоследовательность в событиях. При вызове processEvents() сигнал таймера не только будет добавлен в очередь, но и тут же выполнен после тех событий, что уже успели быть добавленными в очередь. Иными словами, Ваша атомарная транзакция "0-1-2" будет нарушена, если, скажем, таймер сработает между "0" и "1", и в это же время проследует вызов processEvents(). –  Jun 20 '16 at 13:06
4

Немного издали... Если сигнал подключается к слоту с флагом Qt::AutoConnection (используется по умолчанию) или Qt::DirectConnection, и вызываемый слот принадлежит объекту, находящемуся в том же потоке, что и объект, от которого исходит сигнал, то никакой обработчик очереди событий не вызывается. Производится вызов слота прямо на месте. Если этот слот посредством прямого подключения через сигнал будет вызывать сам себя, то случится бесконечная рекурсия, вплоть до переполнения стека, ну и конечно неизбежного краха приложения.

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

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

Все события обрабатываются строго по очереди. Здесь как никогда верно правило: "Кто первый встал, того и тапки". Это означает, что если таймер создан с точностью, допускающей определённую погрешность в интервале, то события таймера будут поступать на обработку с непредсказуемым (в рамках погрешности, разумеется) интервалом и могут попасть в очередь до или после любого другого события.

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

Однако существуют подводные камни, возможность появления которых стоит иметь в виду. Предположим, имеется следующая схема:

Событие А: слот СЛОТ вызывает сам себя через очередь событий;

Событие Б: таймер вызывает СЛОТ с неким интервалом;

Получится следующее:

Вызовы А будут идти друг за другом, как и положено. Поскольку каждое новое событие всегда встаёт вконец очереди, то вызов Б через какое-то время без проблем вклинится в порядок вызовов А. Однако каждый вызов слота СЛОТ всегда порождает новый вызов А. Таким образом, после каждого Б будет порождаться не один новый А, а второй, третий (и т.д.) бесконечные циклы из А.

  • Спасибо за подробный ответ. Надо понимать что функция вызывающая сама себя через сигнал - это некая условность чтобы описать ситуацию. Мне сейчас больше интересует в случае таймера подключенного сигналом не на прямую, а очередью, в какой момент в очередь будет добавляться сигнал таймаут? В момент возврата потока к очереди сообщений или в любой произвольный момент? – Andrey Golikov Jun 20 '16 at 11:36
  • @AndreyGolikov , событие о том, что время по таймеру истекло, будет создано в приложении и добавлено в очередь событий тогда, когда поток вернёт управление т.н. менеджеру очереди событий, наследнику QAbstractEventDispatcher. Это тот, что выполняется по QApplication::exec(). –  Jun 20 '16 at 11:47
  • А для таймера живущего в другом потоке? Как я понимаю в exec() соответствующего потока. По этой причине, вероятно, таймеры требуют наличия exec() для работы вне главного потока. – Andrey Golikov Jun 20 '16 at 12:42
  • @AndreyGolikov , QAbstractEventDispatcher - он просто смотрит, пришли ли какие в приложение нативные события от операционной системы и пересоздаёт их уже как собственные объекты в качестве наследников QEvent. По этой причине очень легко заморозить графический интерфейс пользователя, если вовремя не передавать управление менеджеру событий. Тот же механизм и в потоках, отличных от главного. Менеджер событий смотрит, что объект таймера живёт в том же потоке, что и он сам, и тогда создаёт соответствующий объект события. –  Jun 20 '16 at 12:57