Введение
QTimer — один из самых часто используемых классов Qt, но его поведение в многопоточном контексте вызывает регулярную путаницу: таймер привязан к event loop конкретного потока, и попытка использовать его «напрямую» из другого потока приводит к тому, что он либо не срабатывает вовсе, либо срабатывает не в том потоке, где ожидалось. В статье разберём, как корректно работать с таймерами в многопоточных приложениях.
Концепция
QTimer (как и любой QObject) принадлежит потоку, в котором он создан, и для срабатывания требует работающего event loop именно в этом потоке. Если создать QTimer в основном потоке, а затем попытаться запустить его (start()) из рабочего потока без moveToThread, таймер всё равно будет привязан к основному потоку, и попытка вызвать его методы из другого потока без предосторожностей небезопасна (нарушает однопотоковость доступа к QObject).
Правильный подход — либо создавать и запускать таймер непосредственно в том потоке, где он должен работать (что требует наличия там event loop, то есть QThread::exec() или собственного цикла обработки событий), либо использовать статические методы вроде QTimer::singleShot(), которые удобны, но всё равно требуют, чтобы вызывающий поток имел event loop.
Пример кода
#include <QCoreApplication>
#include <QThread>
#include <QTimer>
#include <QDebug>
class PeriodicWorker : public QObject
{
Q_OBJECT
public:
void start()
{
// Таймер создаётся ВНУТРИ слота, который выполнится
// уже в потоке Worker после moveToThread + thread->start()
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &PeriodicWorker::onTick);
m_timer->start(500);
qDebug() << "Таймер запущен в потоке" << QThread::currentThreadId();
}
private slots:
void onTick()
{
qDebug() << "Тик таймера в потоке" << QThread::currentThreadId();
}
private:
QTimer *m_timer = nullptr;
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
auto *thread = new QThread;
auto *worker = new PeriodicWorker;
worker->moveToThread(thread);
// ВАЖНО: запуск таймера происходит через started(), то есть
// уже после того, как объект "переехал" в новый поток и там
// запустился event loop
QObject::connect(thread, &QThread::started, worker, &PeriodicWorker::start);
thread->start();
QTimer::singleShot(3000, &app, [thread, worker]() {
thread->quit();
thread->wait();
worker->deleteLater();
thread->deleteLater();
qApp->quit();
});
return app.exec();
}
Пояснения к коду
Ключевой момент примера — таймер m_timer создаётся не в конструкторе PeriodicWorker (который выполняется в основном потоке при создании объекта), а в методе start(), вызванном через сигнал QThread::started уже после того, как объект был перемещён в новый поток и этот поток запустил собственный event loop. Это гарантирует, что QTimer создаётся и живёт именно в рабочем потоке, и его сигнал timeout() будет доставлен и обработан там же.
Главный поток в это время продолжает работать со своим собственным QTimer::singleShot, который через 3 секунды останавливает рабочий поток и завершает приложение — оба таймера работают полностью независимо, каждый в своём потоке.
Подводные камни
- Создание
QTimerв конструкторе объекта, который затем перемещается в другой поток черезmoveToThread. Хотя сам объектQTimerкорректно «переедет» вместе со своим родителем, более безопасный и явный паттерн — создавать таймер непосредственно в коде, который точно выполняется в целевом потоке (как в примере, через слотstarted), чтобы не зависеть от тонкостей перемещения дочерних объектов. - Вызов
timer->start()из чужого потока. МетодыQTimer, не являющиеся потокобезопасными по документации, нельзя безопасно вызывать из потока, отличного от того, которому принадлежит объект — для управления таймером из другого потока нужно использоватьQMetaObject::invokeMethod()сQt::QueuedConnection. - Отсутствие event loop в рабочем потоке. Если в
QThread::run()переопределён без вызоваexec()(то есть поток выполняет линейный код без цикла обработки событий), таймеры, созданные в этом потоке, никогда не сработают —QTimerпринципиально зависит от event loop. - Точность таймеров не гарантирована под нагрузкой.
QTimer— программный, а не аппаратный таймер; если event loop потока перегружен другими задачами (длинные слоты, тяжёлые вычисления в том же потоке), фактическое время срабатывания таймера может существенно отклоняться от заданного интервала.