[Development] QTimer question

Jaroslaw Kobus Jaroslaw.Kobus at qt.io
Tue May 30 23:03:26 CEST 2023


[Re-sending my reply to Thiago to the mailing list, as I think it may be possibly interesting for more people.]
[In meantime I've created https://codereview.qt-project.org/c/qt/qtbase/+/480703 and waiting for CI results.]

> On Monday, 29 May 2023 11:40:14 PDT Jaroslaw Kobus via Development wrote:
> > Hi All,
> > 
> > when I start 2 single shot timers synchronously in a row, with exactly the
> > same interval, from the same thread, can I rely on having their handlers
> > called in the same order in which they were started?
> > 
> > I.e.:
> > 
> > QTimer::singleShot(1000, [] { qDebug() << "1st timer elapsed"; });
> > QTimer::singleShot(1000, [] { qDebug() << "2nd timer elapsed"; });
> > 
> > Should I always see the:
> > 
> > 1st timer elapsed
> > 2nd timer elapsed
> > 
> > on the output, or should I expect the random order?
> 
> Strictly speaking, no, you can't. Because timers aren't precise, timers can be 
> delivered early or late by 5% if they are Coarse. Therefore, the order will 
> depend on which direction the rounding went, which depends on how long it took 
> you between scheduling those two timers.
> 
> It will depend on the timeout too. For single-shot timers, timers below 2 
> seconds are Precise, which means only those above that threshold will suffer 
> reordering.
> 
> For non-QTimer::singleShot, all timers are Coarse by default.

Hi Thiago,

thank you for your answer. There are 3 main points:

1. Coarsing is OK and I'm totally fine with having my timers called a bit
earlier or later (within 5% accuracy).

However, this doesn't explain why I can't rely on the proper order in the given example.
I can imagine that the expected order is preserved even when having coarse timers.
The simple implementation could look like:

Keeping a global hash (per thread?) of started timers together
with their expected timeout timestamp, and the opposite map:

QHash<timerId, expectedTimeoutTimestamp> timerIdToDeadline;
QMap<expectedTimeoutTimestamp, timerId> deadlineToTimerId;

Whenever calling the singleShot(), the implementation should add and item to the map and hash.
The expectedTimeoutTimestamp would be set:
expectedTimeoutTimestamp = currentTime + timerInterval;

Whenever any timer (timer X) timeouts, and before it emits a timeout signal publicly,
it could lookup the hash for its expectedTimeoutTimestamp.
Before emitting the timer X's timeout, it should emit timeouts for timers that have their
expectedTimeoutTimestamp lower than the timer X's expectedTimeoutTimestamp
(i.e. those, that are before the timer X's expectedTimeoutTimestamp inside the deadlineToTimerId map).
In this case, all the extra timers that emitted their timeout should be removed
from map and hash, and later, when theirs internal activation signal comes - the public emission
should be omitted.

When adding a mutex to the map and hash, it could probably also work for all threads.

Recently I was rewriting the autotests for the TaskTree
(https://doc-snapshots.qt.io/qtcreator-extending/tasking-tasktree.html),
and wanted to use the simplest possible asynchronous task, instead of a concurrent call.
I've concluded it must be a singleShot(). Inside the tests I need to rely on the proper order
of timers' timeouts. It would be cool to have it in Qt. Anyway, it looks like currently I need
to implement the above solution on my own (at least until it's done in Qt, if decided so).

I may create a task request ticket for it, if you want me to.

2. Regarding:

> It will depend on the timeout too. For single-shot timers, timers below 2 
> seconds are Precise, which means only those above that threshold will suffer 
> reordering.

It doesn't look so, unfortunately. I've written the following test:

        QTimer::singleShot(1, this, [&] { log.append(1); });
        QTimer::singleShot(1, this, [&] { log.append(2); });

Both timeouts are 1ms, and I'm observing the opposite order on CI machine:
https://codereview.qt-project.org/c/qt-creator/qt-creator/+/480663/1

Moreover, if I change it to:

        QTimer::singleShot(1, this, [&] { log.append(1); });
        QTimer::singleShot(2, this, [&] { log.append(2); });

I still observe the opposite order, even when the second shot, executed later,
with 100% longer interval, may still come before the first:
https://codereview.qt-project.org/c/qt-creator/qt-creator/+/480663/2
(the same change, different patchset). So, in this case it looks like the 5%
tolerance is beaten considerably up to 100%.

I may create a bugreport for it, if you want me to.

3. Despite of how 1. and 2. will be proceeded, I think we should clearly document it.
It's really not obvious that Qt users can't rely on the proper order of timeouts.

I may create a bugreport for doc team, if you want me to.

Best regards

Jarek


More information about the Development mailing list