Последнее обновление:
August 20, 2019

Есть мысль... Жми, напиши!
Что имеем: Постов : 165 Авторов: 1 Категорий: 27

Qt, вызов функции с ограничением по времени выполнения (QtConcurrent::run timeout).

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

В общем задача: запустить функцию, по возможности дождаться результат, но не дольше N, если не удалось (время вышло), поставить флаг, что не удалось, что бы потом запросить отдельно.

Можно было бы вообще всё использование вынести в отдельный поток, но городить ради одного места неохото, зато родились вот такие извращения:

  1. В Qt есть QtConcurrent, с помощью которого можно запустить функцию в отдельном потоке,
  2. Так как нам желательно сразу получить результат не выходя из функции (в случае ошибки вызываемая функция особо не задерживается), то можно прописать future.result(), но тогда мы будем ждать до победного, а нам нужно ограничить время выполнения, поэтому
  3. Используем QTimer, что бы по истечению заданного времени завершить выполнение, но и тут облом — мы не можем принудительно завершить выполнение «QtConcurrent::run», (т.к. вызываемая функция может не освободить память/не закрыть доступ к файлу да и вообще хз что). Поэтому завершать выполнение не будем, а тупо забьём на него — пусть там себе в параллельном потоке выполняется, а мы тем временем пойдём дальше, но тогда будет необходимо использовать сигналы, что бы дать понять, когда пора выходить из функции по истечению времени, а когда функция вернула результат, поэтому
  4. Используем QFutureWatcher, что бы подписаться на сигнал «finished» если функция выполнилась.
  5. Ну а что бы ждать сигнала от таймера, либо сигнала о завершении вызываемой функции и не выходить до этого из текущей функции используем  QEventLoop.  Как только тот или другой сигнал будет вызван и обработан, то завершаем цикл.

Из преимуществ: выполнение происходит в отдельном потоке, поэтому не затрагиваем основной процесс, в котором происходит вызов функции.

Из недостатков: мы не управляем параллельным процессом в полной мере, т.е. не можем принудительно завершить процесс и он будет болтаться в памяти до исключения, завершения или посинения (если функция «зациклится»).

В общем, вот так:

#include <QtConcurrent/QtConcurrentRun>
#include <QFuture>
#include <QFutureWatcher>
#include <QEventLoop>
#include <QTimer>
#include <QDebug>

....

void AppCore::runAsync()
{
    qDebug()<<"Run async";

    QString r;
    QFuture<QString> f;

    try {
        f = QtConcurrent::run(this, &AppCore::asyncFunction);
    } catch(...) {
        qDebug()<<"ERROR";
    }

    QFutureWatcher<QString> w;
    QEventLoop l;
    QTimer t;

    connect(&w, &QFutureWatcher<QString>::finished, [&l, &r, &w](){
        r = w.result();
        qDebug()<<"From connection 1.";
        l.quit();
    });

    connect(&t, &QTimer::timeout, [&l, &r](){
        r = "TIME OUT!";
        qDebug()<<"From connection 2.";
        l.quit();
    });

    w.setFuture(f);
    t.start(2000);
    l.exec();

    qDebug()<<"RESULT:"<<r;

}

/**
* @brief Функция, которая будет выполнятся в фоновом потоке
* @return
*/
QString AppCore::asyncFunction()
{
    qDebug()<<"In async function.";
    QThread::sleep(5);

    qDebug()<<"ASYNC FUNCTION COMPLETED!";

    return QString("COMPLETED NORMAL!");
}

Вывод:

Run async
In async function.
From connection 2.
RESULT: "TIME OUT!"
ASYNC FUNCTION COMPLETED!

Т.е. вышло время, мы получили результат, что время вышла, но сама вызываемая функция в параллельном потоке всё равно выполнилась до конца.

Ну и упростим это до макроса, что бы иногда использовать:

/**
* Выполняем функцию в параллельном потоке и отдаём результат,
* если не выполнится за время timeOut_ms, то отдаём timeOut_result
* QtConcurrentRunTimeOut(int, myIntRes, this, &MyClass::myFunction, 2000, -100);
* QtConcurrentRunTimeOut(int, myIntRes, this, &MyClass::myFunctionWithParams, 2000, -100,  14, 88);
* qDebug()<<"RESULT:"<<myIntRes;
* Need include
* #include <QtConcurrent/QtConcurrentRun>
* #include <QFuture>
* #include <QFutureWatcher>
* #include <QEventLoop>
* #include <QTimer>
*/
#define QtConcurrentRunTimeOut(resultType, res, object, functionPointer, timeOut_ms, timeOut_result, ...)\
    resultType _r##res;\
    QFuture<resultType> _f##res;\
    _f##res = QtConcurrent::run(object, functionPointer,##__VA_ARGS__);\
    QFutureWatcher<resultType> _w##res;\
    QEventLoop _l##res;\
    QTimer _t##res;\
    connect(&_w##res, &QFutureWatcher<resultType>::finished, [&_l##res, &_r##res, &_w##res](){\
        _r##res = _w##res.result();\
        _l##res.quit();\
    });\
    connect(&_t##res, &QTimer::timeout, [&_l##res, &_r##res](){\
        _r##res = timeOut_result;\
        _l##res.quit();\
    });\
    _w##res.setFuture(_f##res);\
    _t##res.start(timeOut_ms);\
    _l##res.exec();\
    resultType res=_r##res;

Вообще, лучше хорошенько подумать, действительно ли вам нужны такие извращения и может быть лучше переделать архитектуру и сделать по-нормальному выполнение в параллельном процессе с использованием сигнал/слотов.

Вот как-то так =) 

Views :

83

Qt, QEventLoop and connect/disconnect lambda function

Приветствую!

Кому некогда, можно сразу прыгнуть в конец к итогу.

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

Views :

463