Что происходит с сигналами Qt, когда приемник занят?

В моем приложении у меня есть экземпляр QTimer, сигнал которого timeout() подключен к слоту в главном объекте окна, заставляя его периодически вызываться. Слот снимает изображение с камеры и сохраняет его на диск.

Мне было интересно, что произойдет, если сигнал испущен (из отдельного потока, где QTimer выполняется, я полагаю), когда приемник (объект окна в основном потоке) в настоящее время занят (например, при взятии и сохранении предыдущего картина). Вызывается ли вызов в очереди и выполняется после завершения предыдущего вызова? Вся идея состоит в том, чтобы запустить ее через регулярные промежутки времени, но могут ли эти вызовы стоять в очереди, а затем вызываться случайным образом, когда управление возвращается в цикл событий, вызывая беспорядок? Как я могу избежать этого? Теоретически слот должен выполняться быстро, но, скажем, у аппаратного обеспечения возникла какая-то проблема, и там был ларек.

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

Ответ 1

Другие ответы на данный момент имеют соответствующий контекст. Но главное знать, что если обратный вызов таймера сигнализирует слот в другом потоке, то это соединение является либо QueuedConnection, либо BlockingQueuedConnection.

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

Таймер должен находиться в том же потоке, что и логика фото. Включение таймера в тот же поток, что и камера, делает соединение прямым и обеспечивает лучшую стабильность ваших интервалов времени. Особенно, если захват и сохранение фотографий имеют случайные исключительные продолжительности.

Это происходит примерно так: предположим, что интервал составляет 10 секунд:

  •   
  • установить таймер на 10 секунд  
  • срабатывает таймер  
  • сохранить время запуска  
  • взять фото  
  • сохранить фото на диск (скажем, требуется 3 секунды по какой-то нечетной причине)  
  • вычислить 10- (текущее время - время начала) = семь секунд  
  • установить тайм-аут на семь секунд

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

Ответ 2

Здесь я подробно расскажу о том, как QTimer ведет себя, когда приемник занят.

Вот исходный код эксперимента: (добавьте QT += testlib в файл проекта)

#include <QtGui>
#include <QtDebug>
#include <QTest>

struct MyWidget: public QWidget
{
    QList<int> n;    // n[i] controls how much time the i-th execution takes
    QElapsedTimer t; // measure how much time has past since we launch the app

    MyWidget()
    {
        // The normal execution time is 200ms
        for(int k=0; k<100; k++) n << 200; 

        // Manually add stalls to see how it behaves
        n[2] = 900; // stall less than the timer interval

        // Start the elapsed timer and set a 1-sec timer
        t.start();
        startTimer(1000); // set a 1-sec timer
    } 

    void timerEvent(QTimerEvent *)
    {
        static int i = 0; i++;

        qDebug() << "entering:" << t.elapsed();
        qDebug() << "sleeping:" << n[i]; QTest::qSleep(n[i]);
        qDebug() << "leaving: " << t.elapsed() << "\n";
    }   
};  

int main(int argc, char ** argv)
{
    QApplication app(argc, argv);   
    MyWidget w;
    w.show();
    return app.exec();
}

Когда время выполнения меньше интервала времени

Затем, как и следовало ожидать, таймер работает непрерывно каждую секунду. Он учитывает, сколько времени выполнено выполнение, а затем метод timerEvent всегда начинается с кратного 1000 мс:

entering: 1000 
sleeping: 200 
leaving:  1201 

entering: 2000 
sleeping: 900 
leaving:  2901 

entering: 3000 
sleeping: 200 
leaving:  3201 

entering: 4000 
sleeping: 200 
leaving:  4201 

Если только один клик пропущен, потому что приемник занят

n[2] = 1500; // small stall (longer than 1sec, but less than 2sec)

Затем следующий слот вызывается сразу после завершения стойла, но последующие вызовы по-прежнему кратно 1000 мс:

entering: 1000 
sleeping: 200 
leaving:  1200 

entering: 2000 
sleeping: 1500 
leaving:  3500 // one timer click is missed (3500 > 3000)

entering: 3500 // hence, the following execution happens right away
sleeping: 200 
leaving:  3700 // no timer click is missed (3700 < 4000)

entering: 4000 // normal execution times can resume
sleeping: 200 
leaving:  4200 

entering: 5000 
sleeping: 200 
leaving:  5200 

Он также работает, если следующие клики также пропущены из-за накопления времени, пока есть только один клик, который пропущен при каждом выполнении:

n[2] = 1450; // small stall 
n[3] = 1450; // small stall 

выход:

entering: 1000 
sleeping: 200 
leaving:  1201 

entering: 2000 
sleeping: 1450 
leaving:  3451 // one timer click is missed (3451 > 3000)

entering: 3451 // hence, the following execution happens right away
sleeping: 1450 
leaving:  4901 // one timer click is missed (4901 > 4000)

entering: 4902 // hence, the following execution happens right away
sleeping: 200 
leaving:  5101 // one timer click is missed (5101 > 5000)

entering: 5101 // hence, the following execution happens right away
sleeping: 200 
leaving:  5302 // no timer click is missed (5302 < 6000)

entering: 6000 // normal execution times can resume
sleeping: 200 
leaving:  6201 

entering: 7000 
sleeping: 200 
leaving:  7201 

Если пропущено более одного щелчка, потому что приемник был очень занят

n[2] = 2500; // big stall (more than 2sec)

Если пропущено два или более кликов, появляется только проблема. Время выполнения не синхронизируется с первым исполнением, а скорее с момента окончания стойла:

entering: 1000 
sleeping: 200 
leaving:  1200 

entering: 2000 
sleeping: 2500 
leaving:  4500 // two timer clicks are missed (3000 and 4000)

entering: 4500 // hence, the following execution happens right away
sleeping: 200 
leaving:  4701 

entering: 5500 // and further execution are also affected...
sleeping: 200 
leaving:  5702 

entering: 6501 
sleeping: 200 
leaving:  6702 

Заключение

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

entering: 1000 
sleeping: 200 
leaving:  1200 

entering: 2000 
sleeping: 1500 
leaving:  3500 // one timer click is missed 

entering: 4000 // I don't want t execute the 3th execution
sleeping: 200 
leaving:  4200 

Затем вы можете использовать тривиальную реализацию и просто проверьте, что enteringTime < expectedTime + epsilon. Если это правда, сделайте снимок, если он ложный, ничего не делает.

Ответ 3

Вы можете использовать тип соединения Qt::(Blocking)QueuedConnection для метода подключения, чтобы избежать немедленных немедленных прямых подключений.

Поскольку у вас есть отдельные потоки, вы должны использовать блокирующую версию. Тем не менее, вы должны учитывать неблокирующий вариант, когда вы хотите избежать прямых вызовов без отдельного потока для получателя.

Подробнее см. официальную документацию.

Из документации для вашего удобства:

Qt:: QueuedConnection

Слот вызывается, когда управление возвращается к циклу событий потока приемника. Слот выполняется в потоке приемника.

Qt:: BlockingQueuedConnection

То же, что и QueuedConnection, за исключением того, что текущий поток блокируется до тех пор, пока слот не вернется. Этот тип подключения должен использоваться только там, где эмиттер и приемник находятся в разных потоках.

То, что вы, вероятно, хотели написать, это то, что вам не хотелось бы иметь прямое соединение, а не в очереди.

QCoreApplication::removePostedEvents ( QObject * receiver, int eventType ) с типом события MetaCall можно использовать или очищать очередь, если она насыщается этими тяжелыми задачами. Кроме того, вы всегда можете использовать флаг для связи с этим слотом для выхода, если он установлен.

Подробнее см. в следующем обсуждении форума: http://qt-project.org/forums/viewthread/11391

Ответ 4

Ответ: да. Когда ваш QTimer и ваш ресивер находятся в разных потоках, вызов помещается в очередь событий получателей. И если ваша процедура съемки или сохранения изображений - это время выполнения теста, ваше событие может быть отложено очень сильно. Но это одно и то же для всех событий. Если подпрограмма не возвращает управление циклу событий, ваш gui зависает. Вы можете использовать:

Qt:: BlockingQueuedConnection То же, что и QueuedConnection, кроме текущие блоки потока до тех пор, пока слот не вернется. Этот тип подключения следует использовать только там, где эмиттер и приемник находятся в разных потоки.

Но, скорее всего, подобная ситуация - это намек на то, что с вашей логикой что-то не так.