Выполняется ли объект QTimer в отдельном потоке? Каков его механизм?

Когда я создаю объект QTimer в Qt 5 и запускаю его с помощью функции-члена start(), создается ли отдельный поток, который отслеживает время и вызывает функцию timeout() через равные промежутки времени?

Например,

QTimer *timer = new QTimer;
timer->start(10);
connect(timer,SIGNAL(timeout()),someObject,SLOT(someFunction()));

Здесь, как программа узнает, когда происходит timeout()? Я думаю, что это должно было бы выполняться в отдельном потоке, так как я не вижу, как последовательная программа могла бы отслеживать время и продолжать выполнение одновременно. Однако я не смог найти какую-либо информацию по этому поводу ни в документации Qt, ни где-либо еще, чтобы подтвердить это.

Я прочитал официальную документацию, и некоторые вопросы по StackOverflow, такие как эта и эта, похоже, очень связаны, но я не смог получить ответ через них.

Может ли кто-нибудь объяснить механизм, с помощью которого работает объект QTimer?

В процессе поиска я обнаружил, что согласно этому ответу Билла упоминается, что

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

Означает ли это, что timeout() обрабатывается ОС? Есть ли какое-то оборудование, которое отслеживает время и отправляет прерывания с соответствующими интервалами? Но если это так, то как много таймеров могут работать одновременно и независимо, как можно отслеживать каждый таймер отдельно?

Какой механизм?

Спасибо.

Ответ 1

Когда я создаю объект QTimer в Qt 5 и запускаю его с помощью функции start() функция-член, представляет собой отдельный поток, созданный, который отслеживает время и вызывает функцию таймаута() через регулярные интервалы?

Нет; создание отдельного потока было бы дорогостоящим, и это не обязательно, так что не реализовано QTimer.

Здесь, как программа знает, когда происходит таймаут()?

Метод QTimer:: start() может вызывать функцию системного времени (например, gettimeofday() или аналогичный), чтобы узнать (с точностью до несколько миллисекунд), что было вызвано тем, что был вызван start(). Затем он может добавить к этому времени десять миллисекунд (или любое другое значение), и теперь у него есть запись, указывающая, когда ожидается, что сигнал таймаута() будет испускаться следующим образом.

Итак, имея эту информацию, что она делает, чтобы убедиться, что это происходит?

Ключевым фактом, который следует знать, является то, что тайм-аут QTimer-сигнал-излучение работает только в том случае, если/когда ваша программа Qt выполняется внутри цикла событий Qt. Почти каждая программа Qt будет иметь что-то вроде этого, обычно рядом с ее функцией main():

QApplication app(argc, argv);
[...]
app.exec();

Обратите внимание, что в типичном приложении почти все время приложения будет потрачено внутри этого вызова exec(); то есть вызов app.exec() не будет возвращаться до тех пор, пока не истечет время выхода приложения.

Итак, что происходит внутри этого вызова exec() во время работы вашей программы? С большой сложной библиотекой, такой как Qt, она обязательно сложна, но не слишком просто упростить, чтобы сказать, что она запускает цикл событий, который выглядит концептуально примерно так:

 while(1)
 {
     SleepUntilThereIsSomethingToDo();  // not a real function name!
     DoTheThingsThatNeedDoingNow();     // this is also a name I made up
     if (timeToQuit) break;
 }

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

Так как же SleepUntilThereIsSomethingToDo() знает, когда пришло время проснуться и вернуться? Это сильно изменится в зависимости от того, на какой ОС вы работаете, поскольку у разных ОС есть разные API-интерфейсы для работы с подобными вещами, но классический способ UNIX-y реализовать такую ​​функцию будет с POSIX выберите() звонок:

int select(int nfds, 
           fd_set *readfds, 
           fd_set *writefds,
           fd_set *exceptfds, 
           struct timeval *timeout);

Обратите внимание, что select() принимает три разных аргумента fd_set, каждый из которых может указывать количество дескрипторов файлов; путем передачи в соответствующие объекты fd_set этим аргументам вы можете вызвать select() для пробуждения момента, когда операции ввода-вывода становятся возможными в любом из нескольких дескрипторов файлов, которые вы хотите отслеживать, чтобы ваша программа могла обрабатывать I/O без задержки. Однако интересная часть для нас - это последний аргумент, который является аргументом тайм-аута. В частности, вы можете передать объект struct timeval здесь, который говорит select(): "Если после (этого много) микросекунд не произошло событий ввода-вывода, тогда вы должны просто отказаться и вернуться в любом случае".

Это оказывается очень полезным, потому что, используя этот параметр, функция SleepUntilThereIsSomethingToDo() может делать что-то вроде этого (псевдокод):

void SleepUntilThereIsSomethingToDo()
{
   struct timeval now = gettimeofday();  // get the current time
   struct timeval nextQTimerTime = [...];  // time at which we want to emit a timeout() signal, as was calculated earlier inside QTimer::start()
   struct timeval maxSleepTimeInterval = (nextQTimerTime-now);
   select([...], &maxSleepTimeInterval);  // sleep until the appointed time (or until I/O arrives, whichever comes first)
}

void DoTheThingsThatNeedDoingNow()
{
   // Is it time to emit the timeout() signal yet?
   struct timeval now = gettimeofday();
   if (now >= nextQTimerTime) emit timeout();

   [... do any other stuff that might need doing as well ...]
}   

Надеюсь, это имеет смысл, и вы можете видеть, как цикл события использует аргумент timeout select(), чтобы он мог проснуться и испускать сигнал таймаута() в (приблизительно) времени, которое он ранее вычислял при вызове start().

Btw, если приложение имеет одновременно несколько активных QTimer, что не проблема; в этом случае SleepUntilThereIsSomethingToDo() просто нужно перебрать все активные QTimers, чтобы найти ту, которая имеет наименьшую отметку времени следующего таймаута, и использовать только ту минимальную метку времени для вычисления максимального интервала времени, который выбирает() должно быть разрешено спать. Затем после возвращения select() DoTheThingsThatNeedDoingNow() также выполняет итерацию по активным таймерам и испускает сигнал тайм-аута только для тех, чья печать следующего тайма-времени не больше текущего времени. Цикл событий повторяется (как можно быстрее или медленнее, если необходимо), чтобы придать видимость многопоточного поведения, фактически не требуя нескольких потоков.

Ответ 2

Глядя на документацию о таймерах и в исходный код из QTimer и QObject мы видим, что таймер работает в цикле потока/события, который назначается объекту. Из документа:

Для работы QTimer у вас должен быть цикл событий в вашем приложении; то есть вы должны называть QCoreApplication::exec() где-то. События таймера будут доставлены только при запуске цикла событий.

В многопоточных приложениях вы можете использовать QTimer в любом потоке, который имеет цикл событий. Чтобы запустить цикл событий из потока, отличного от GUI, используйте QThread::exec(). Qt использует аффинность потока таймера, чтобы определить, какой поток будет излучать сигнал timeout(). Из-за этого вы должны запустить и остановить таймер в своем потоке; невозможно запустить таймер из другого потока.

Внутри QTimer просто использует метод QObject::startTimer для запуска через определенное время. Этот сам как-то говорит, что поток, который он запускает, загорается после количества времени.

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

Ответ 3

Объект QTimer регистрируется в EventDispatcher (QAbstractEventDispatcher), который заботится о том, чтобы отправлять события типа QTimerEvent каждый раз, когда для определенного зарегистрированного QTimer истекает время ожидания. Например, в GNU/Linux есть частная реализация QAbstractEventDispatcher, называемая QEventDispatcherUNIXPrivate, которая производит расчеты с учетом API платформы на данный момент времени. QTimerEvent отправляются из QEventDispatcherUNIXPrivate в очередь цикла событий того же потока, к которому принадлежит объект QTimer, то есть был создан. Также,

QEventDispatcherUNIXPrivate не запускает QTimerEvent из-за какого-либо системного события ОС, а потому, что периодически проверяет время ожидания, когда processEvents вызывается циклом события потока, где также находится QTimer. См. Здесь: https://code.woboq.org/qt5/qtbase/src/corelib/kernel/qeventdispatcher_unix_p.h.html#QEventDispatcherUNIXPrivate