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

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

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

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


person neuviemeporte    schedule 07.09.2013    source источник
comment
Моя интуиция — подумать о переносе логики, которая делает снимок и сохраняет на диск, из основного потока графического интерфейса в отдельный поток. Это должно упростить работу потока фотографий с регулярными интервалами, установив собственный таймер на оставшееся время интервала после сохранения фотографии.   -  person Digikata    schedule 08.09.2013
comment
@Digikata: На самом деле, это устроено так, что поток графического интерфейса владеет таймером и имеет слот, который периодически вызывается, но все, что делает этот слот, это посылает сигнал для другого потока, который заботится о захвате и сохранении материала.   -  person neuviemeporte    schedule 08.09.2013
comment
есть небольшая проблема с этим, если вы пытаетесь получить твердые временные интервалы - я расширю ответ...   -  person Digikata    schedule 08.09.2013


Ответы (4)


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

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

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

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

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

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

person Digikata    schedule 07.09.2013
comment
Спасибо, что вернули нас в нужное русло. :) В любом случае, я понимаю, что вы выступаете за использование singleShot() таймеров здесь. Хорошая идея с обновленным интервалом, я бы также знал, если бы фотография занимала более одного интервала, если бы значение было отрицательным... Нужно обработать это. :) - person neuviemeporte; 08.09.2013
comment
Что не так с простейшим решением, не использующим потоки и использующим таймер с фиксированным интервалом в 10 секунд? Можете ли вы уточнить это? Он учитывает, сколько времени требуется для выполнения слота, подключенного к тайм-ауту. Если время между текущим временем и последним предполагаемым выполнением больше 10 секунд, ничего не делать вместо того, чтобы делать снимок (это очищает очередь событий от всех ожидающих событий, если таковые имеются). Я довольно часто использую его для рендеринга OpenGL с фиксированной скоростью, и он работает невероятно точно (я измерял), даже если слот рендеринга занимает разное время при каждом выполнении. - person Boris Dalstein; 08.09.2013
comment
@Boris Более простое решение предпочтительнее, если нет риска, что выполняемая вами обработка может выйти за пределы интервала, и вы хотите возобновить обработку, придерживаясь исходных границ временного интервала. Первоначальный постер намекал, что могут быть исключительные случаи, такие как зависание оборудования при захвате фотографий, и, похоже, беспокоился об этих границах. - person Digikata; 08.09.2013
comment
Как я уже сказал, я на 90% уверен, что даже в этом случае вам не придется трогать интервал таймера. Просто проверьте в слоте, было ли последнее выполнение слишком длинным (например, было 35 секунд), и если это так, ничего не делайте для следующих n выполнений (3 выполнения в этом случае), и таймер вернется, чтобы выдать тайм-аут как несколько t0+10*k, где t0 — время начала. Но я попробую, когда у меня будет время, чтобы быть уверенным на 100%, и опубликую это как ответ, если я прав. (В качестве комментария здесь, если я ошибаюсь ;-)) - person Boris Dalstein; 08.09.2013
comment
+1 ;-) Я провел несколько экспериментов. Вы действительно правы, если задержка больше, чем удвоенный интервал таймера (т.е. пропущено два или более щелчка). В противном случае, если может быть пропущен только один щелчок (в такой ситуации я всегда был), таймер правильно подстраивается. Следовательно, если у вас нет верхней границы того, сколько времени может занять остановка (что, по-видимому, имеет место в случае ОП), ваше решение является правильным. - person Boris Dalstein; 09.09.2013
comment
Еще одно замечание: мой опубликованный метод тоже не идеален, в зависимости от того, как реализованы расчеты времени, вы можете увидеть некоторый дрейф в течение длительного общего пробега. Если вы используете абсолютное время для расчета целевого времени начала, вы также можете лучше уменьшить дрейф. - person Digikata; 10.09.2013

После некоторых экспериментов я подробно описал, как 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. Если это правда, сделайте снимок, если это ложь, ничего не делайте.

person Boris Dalstein    schedule 09.09.2013

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

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

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

person Greenflow    schedule 07.09.2013
comment
Вы случайно не знаете, насколько велика очередь? Сколько сигналов может застрять там одновременно? Есть ли способ очистить очередь вручную? - person neuviemeporte; 08.09.2013
comment
@neuviemeporte: QCoreApplication::removePostedEvents ( QObject * Receiver, int eventType ) ... но, конечно, вы всегда можете использовать атомарный флаг для связи со слотом для выхода, если он установлен. Должен ли я включить эту информацию в свой ответ для вас? - person lpapp; 08.09.2013
comment
И нет, я не знаю, насколько глубока очередь событий. А так как, насколько я знаю, это нигде не задокументировано, использовать эту информацию все равно не стоит. Может измениться с любым обновлением Qt. Тип события для сигнала/слотов — Event::MetaCall. Итак, вы можете попробовать QCoreApplication::removePostedEvents, как предлагает Ласло Папп. - person Greenflow; 08.09.2013
comment
Максимальный размер цикла событий задокументирован? Где? - person Greenflow; 08.09.2013
comment
Я не планировал использовать эту информацию (вероятно, ни в коем случае), просто интересно ради любопытства. Я предполагаю, что очередь не бесконечно глубока, в какой-то момент события просто будут отброшены в стиле FIFO. - person neuviemeporte; 08.09.2013
comment
Это одна из причин, по которой я здесь: чтобы столкнуться с вопросами, о которых я никогда не задумывался. Я попытался проверить. В одном неофициальном источнике утверждалось, что очередь ограничена только памятью. Будет иметь смысл. Не знаю, зачем его искусственно ограничивать. Если объекты события помещаются в стек, это может быть ограничением. Я думаю, что завтра я должен изучить исходники qt. Меня это несколько интересует. - person Greenflow; 08.09.2013

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

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

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

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

Qt::QueuedConnection

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

Qt::Блокингкуеуедконнектион

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

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

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

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

person lpapp    schedule 07.09.2013