Приветствую!
Кому некогда, можно сразу прыгнуть в конец к итогу.
Бывают ситуации, когда нужно синхронно дождаться завершения асинхронного действия, при этом не подвешивая основной поток (например, не продолжать выполнение функции, пока ответ в 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 комментария