Приветствую!
Кому некогда, можно сразу прыгнуть в конец к итогу.
Бывают ситуации, когда нужно синхронно дождаться завершения асинхронного действия, при этом не подвешивая основной поток (например, не продолжать выполнение функции, пока ответ в QTCPSocket onReadyRead от сервера не придёт).
В нашем случае для примера давайте подождём с выполнением функции, пока таймер не досчитает до 5.
Делаем основу:
#ifndef APPCORE_H
#define APPCORE_H
#include <QObject>
#include <QTimer>
#include <QGuiApplication>
#include <QDebug>
class AppCore : public QObject
{
Q_OBJECT
public:
explicit AppCore(QObject *parent = nullptr): QObject(parent)
{
_timer = new QTimer(this);
_timer->setSingleShot(true); //-- Срабатывать только один раз
}
void waitFunction()
{
qDebug()<<"Timer BEGIN";
_timer->start(5000); //-- 5 секунд в миллисекундах
qDebug()<<"Timer END";
}
signals:
public slots:
private:
QTimer * _timer;
};
#endif // APPCORE_H
Но если мы запустим так, то вывод «Timer END» произойдёт без какой-либо задержки, т.к. таймер ведёт отсчёт асинхронно в другом потоке.
Решением в «лоб» было бы подписаться на событие срабатывания таймера, объявить флаг срабатывания и в бесконечном цикле отслеживать его, как-то так, например:
#ifndef APPCORE_H
#define APPCORE_H
#include <QObject>
#include <QTimer>
#include <QDebug>
#include <QGuiApplication>
class AppCore : public QObject
{
Q_OBJECT
public:
explicit AppCore(QObject *parent = nullptr): QObject(parent)
{
_timer = new QTimer(this);
_timer->setSingleShot(true); //-- Срабатывать только один раз
connect(_timer, &QTimer::timeout, this, &AppCore::onTimeOut);
}
void waitFunction()
{
qDebug()<<"Timer BEGIN";
_timer->start(5000); //-- 5 секунд в миллисекундах
while (true) {
if (_timeOut) {
break;
}
QGuiApplication::processEvents();
}
qDebug()<<"Timer END";
}
public slots:
void onTimeOut()
{
_timeOut = true;
}
private:
QTimer * _timer;
bool _timeOut=false;
};
#endif // APPCORE_H
Кстати, если бы мы не указали «QGuiApplication::processEvents();«, то слот «onTimeOut()» не вызвался бы никогда, т.к. цикл у нас бесконечный, а так мы заставляем всё таки обработать события, а заодно и не подвешивать сильно интерфейс.
Но решение это слишком топорное и не красивое. Что бы не использовать бесконечные циклы у Qt есть QEventLoop. Выполнение функции приостанавливается методом «exec()» и QEventLoop ждёт, пока не будет вызван метод «exit()» и лишь потом продолжается.
Делаем:
#ifndef APPCORE_H
#define APPCORE_H
#include <QObject>
#include <QTimer>
#include <QDebug>
#include <QGuiApplication>
#include <QEventLoop>
class AppCore : public QObject
{
Q_OBJECT
public:
explicit AppCore(QObject *parent = nullptr): QObject(parent)
{
_timer = new QTimer(this);
_timer->setSingleShot(true); //-- Срабатывать только один раз
connect(_timer, &QTimer::timeout, this, &AppCore::onTimeOut);
}
void waitFunction()
{
qDebug()<<"Timer BEGIN";
_timer->start(5000); //-- 5 секунд в миллисекундах
_loop.exec(); //-- Ждём, пока будет вызван exit();
qDebug()<<"Timer END";
}
public slots:
void onTimeOut()
{
_loop.exit();
}
private:
QTimer * _timer;
QEventLoop _loop;
};
#endif // APPCORE_H
Стало чуть-чуть красивее, но у нас всё ещё висит одноразовый слот «onTimeOut()» и одноразовая переменная «_loop«. Это сейчас она одна, а если в нашем классе нужно в 5 разных местах дожидаться ответов? Как-то по 5 одноразовых слотов и переменных иметь некрасиво…
Благо в Qt начиная с 5 версии появилась возможность при соединении сигнал-слота вместо слота использовать лямбда-функцию, этой фишкой мы и воспользуемся, что бы избавиться он объявления глобального слота «onTimeOut()» и переменной «_loop»
Делаем:
#ifndef APPCORE_H
#define APPCORE_H
#include <QObject>
#include <QTimer>
#include <QDebug>
#include <QGuiApplication>
#include <QEventLoop>
class AppCore : public QObject
{
Q_OBJECT
public:
explicit AppCore(QObject *parent = nullptr): QObject(parent)
{
_timer = new QTimer(this);
_timer->setSingleShot(true); //-- Срабатывать только один раз
}
void waitFunction()
{
qDebug()<<"Timer BEGIN";
QEventLoop _loop;
connect(_timer, &QTimer::timeout, [&](){
_loop.exit();
});
_timer->start(5000); //-- 5 секунд в миллисекундах
_loop.exec(); //-- Ждём, пока будет вызван exit();
qDebug()<<"Timer END";
}
private:
QTimer * _timer;
};
#endif // APPCORE_H
Запускаем и ровно через 5 секунд после «Timer BEGIN» у нас выведется «Timer END» и вроде бы добились чего хотели, но тут есть одна тонкость. Для наглядности я в лямбду добавлю вывод информации о срабатывании, вот так теперь она выглядит::
connect(_timer, &QTimer::timeout, [&_loop](){
qDebug()<<"TIME OUT!";
_loop.exit();
});
Допустим нам нужно в нескольких местах ждать ответа, и мы два раза вызываем функцию:
....
waitFunction();
waitFunction();
....
Вывод будет такой:
Timer BEGIN TIME OUT! Timer END <br>Timer BEGIN TIME OUT! TIME OUT! Timer END
Как видите, «TIME OUT» после второго вызова вывелось два раза! А если бы мы функцию «waitFunction()» вызвали 10 раз, то соответственно «TIME OUT» вывелось бы то же 10 раз подряд. Так явно не должно быть! В чём дело? А дело в том, что лямбда функция автоматически не отключается! Если забыть про эту фишку можно нарваться в том числе и на «SIGSEGV Segmentation fault», сегфолт короче.
Решение — это не забывать отключать (disconnect signal lambda) сигнал от лямбды, но просто так это сделать не получится, т.к. это всё таки лямбда, нужно запоминать информацию, которую возвращает метод «QMetaObject::Connection conn = connet(….)«, а по ней отключать «disconnect(conn)«.
Делаем:
void waitFunction()
{
qDebug()<<"Timer BEGIN";
QEventLoop _loop;
QMetaObject::Connection conn = connect(_timer, &QTimer::timeout, [&_loop](){
qDebug()<<"TIME OUT!";
_loop.exit();
});
_timer->start(5000); //-- 5 секунд в миллисекундах
_loop.exec(); //-- Ждём, пока будет вызван exit();
disconnect(conn);
qDebug()<<"Timer END";
}
Срабатывать будет один раз и отключаться, вывод придёт в норму:
Timer BEGIN TIME OUT! Timer END Timer BEGIN TIME OUT! Timer END
Но об «disconnect(….)» можно забыть, поэтому предлагаю использовать умные указатели, а именно QSharedPointer, но у него нужно не забыть реализовать отключение, т.к. сам по себе он это делать не умеет, а так как писанины получается многовато, поэтому предлагаю запилить макрос.
#ifndef APPCORE_H
#define APPCORE_H
#include <QObject>
#include <QTimer>
#include <QDebug>
#include <QGuiApplication>
#include <QEventLoop>
#include <QSharedPointer>
class AppCore : public QObject
{
Q_OBJECT
public:
explicit AppCore(QObject *parent = nullptr): QObject(parent)
{
_timer = new QTimer(this);
_timer->setSingleShot(true); //-- Срабатывать только один раз
}
#define AutoDisconnect(l) \
QSharedPointer<QMetaObject::Connection> l = QSharedPointer<QMetaObject::Connection>(\
new QMetaObject::Connection(), \
[](QMetaObject::Connection * conn) { /*QSharedPointer сам по себе не производит отключения при удалении*/ \
QObject::disconnect(*conn);\
}\
); *l //-- Use AutoDisconnect(conn1) = connect(....);
void waitFunction()
{
qDebug()<<"Timer BEGIN";
QEventLoop _loop;
AutoDisconnect(conn) = connect(_timer, &QTimer::timeout, [&_loop](){
qDebug()<<"TIME OUT!";
_loop.exit();
});
_timer->start(5000); //-- 5 секунд в миллисекундах
_loop.exec(); //-- Ждём, пока будет вызван exit();
qDebug()<<"Timer END";
}
private:
QTimer * _timer;
};
#endif // APPCORE_H
Вот как-то так =)
P.S. Макросы не зло, нужно просто уметь их готовить 😉

3 комментария
Отличная статья, сразу нашлась по ключевым словам. У себя не стал использовать смартпоинтер, слишком путает код.
Накркомания какая-то тотальная.
1) Размещаем singleshot таймер в куче, а потом страдаем с макросами, чтобы не происходило повторного связывания слота.
2)Используем лямбду с exit’ом в коннекте, хотя есть слот quit().
Что мешало в waitFunction сделать:
QEventLoop loop;
QTimer::singleShot(5000,&loop,&QEventLoop::quit);
qDebug() << "started";
loop.exec();
qDebug() <setSingleShot(true);
Или целью было засовывание всего подряд в пример с малопонятными намерениями?
1. Для примера. В первом же абзаце есть более конкретное пояснение.
2. exit даёт возможность вернуть код, quit то же самое, что exit(0);
3. Такое ощущение, что пост Вы читали по диагонали, я постарался рассказать почему именно воозникли те или иные необходимости. Целью было не простейшее описание QEventLoop, которое и так есть в документации, а более расширенное на реальном примере.