Qt5: Как ждать сигнала в потоке?

Вероятно, заглавный вопрос не очень явный. Я использую Qt5 на Windows7.

В потоке (QThread) в какой-то момент, в "process()" функция / метод, я должен ждать "encrypted()" СИГНАЛ, принадлежащий QSslSocket, который я использую в этой теме. Также я полагаю, что я должен использовать QTimer и ждать "timeout()" СИГНАЛ, чтобы избежать блокировки в бесконечном цикле...
Что у меня сейчас есть:

// start processing data
void Worker::process()
{
    status = 0;
    connect(sslSocket, SIGNAL(encrypted()), this, SLOT(encryptionStarted()));
    QTimer timer;
    connect(&timer, SIGNAL(timeout()), this, SLOT(timerTimeout()));
    timer.start(10000);
    while(status == 0)
    {
        QThread::msleep(5);
    }

    qDebug("Ok, exited loop!");

    // other_things here
    // .................
    // end other_things

    emit finished();
}

// slot (for timer)
void Worker::timerTimeout()
{
    status = 1;
}

// slot (for SSL socket encryption ready)
void Worker::encryptionStarted()
{
    status = 2;
}

Ну, очевидно, это не работает. Он остается в этом цикле времени навсегда...
Итак, вопрос: есть ли способ решить эту проблему? Как я могу ждать этого "encrypted()" СИГНАЛ, но не более - скажем, 10 секунд - чтобы не застрять в этом цикле ожидания / потоке?

5 ответов

Решение

Вы можете использовать локальный цикл обработки событий для ожидания сигнала:

QTimer timer;
timer.setSingleShot(true);
QEventLoop loop;
connect(sslSocket,  SIGNAL(encrypted()), &loop, SLOT(quit()) );
connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
timer.start(msTimeout);
loop.exec();

if(timer.isActive())
    qDebug("encrypted");
else
    qDebug("timeout");

Вот и ждет пока encrypted или время ожидания истекло.

Начиная с Qt 5.0, QSignalSpy предлагает метод ожидания. Вы подключаете его к сигналу, и wait() блокирует его, пока сигнал не сработает.

QSignalSpy spy(SIGNAL(encrypted()));
spy.wait(5000);  //wait until signal fires or 5 second timeout expires

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

Одним из способов реализации этого является разделение ваших действий на отдельные состояния и выполнение некоторой работы при входе в каждое из состояний. Конечно, если объем работы нетривиален, используйте отдельный слот вместо лямбды для удобства чтения.

Обратите внимание на отсутствие явного управления памятью. Использование указателей на классы Qt является преждевременной оптимизацией, и ее следует избегать, когда в этом нет необходимости. Объекты могут быть прямыми членами Worker (или его PIMPL).

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

template <typename Obj>
void instantiateInThread(QThread * thread) {
  Q_ASSERT(thread);
  QObject * dispatcher = thread->eventDispatcher();
  Q_ASSERT(dispatcher); // the thread must have an event loop
  QTimer::singleShot(0, dispatcher, [dispatcher](){
    // this happens in the given thread
    new Obj(dispatcher);
  });
}

Реализация работника:

class Worker : public QObject {
  Q_OBJECT
  QSslSocket sslSocket;
  QTimer timer;
  QStateMachine machine;
  QState s1, s2, s3;
  Q_SIGNAL void finished();
public:
  explicit Worker(QObject * parent = {}) : QObject(parent),
    sslSocket(this), timer(this), machine(this),
    s1(&machine), s2(&machine), s3(&machine) {
    timer.setSingleShot(true);
    s1.addTransition(&sslSocket, SIGNAL(encrypted()), &s2);
    s1.addTransition(&timer, SIGNAL(timeout()), &s3);
    connect(&s1, &QState::entered, [this]{
      // connect the socket here
      ...
      timer.start(10000);
    });
    connect(&s2, &QState::entered, [this]{
      // other_things here
      ...
      // end other_things
      emit finished();
    });
    machine.setInitialState(&s1);
    machine.start();
  }
};

Затем:

void waitForEventDispatcher(QThread * thread) {
  while (thread->isRunning() && !thread->eventDispatcher())
    QThread::yieldCurrentThread();
}

int main(int argc, char ** argv) {
  QCoreApplication app{argc, argv};
  struct _ : QThread { ~Thread() { quit(); wait(); } thread;
  thread.start();
  waitForEventDispatcher(&thread);
  instantiateInThread<Worker>(&myThread);
  ...
  return app.exec();
}

Обратите внимание, что подключение к QThread::started() было бы неестественно: диспетчер событий не существует, пока какой-то код внутри QThread::run() был шанс выполнить. Таким образом, мы должны ждать, пока поток достигнет этого, уступив - очень вероятно, что рабочий поток продвинется достаточно далеко в течение одного или двух выходов. Таким образом, это не будет тратить много времени.

У меня было немного времени в эти дни, и я провел некоторое расследование...
Ну, я просмотрел " http://doc.qt.io/qt-5/qsslsocket.html" и нашел это:

bool QSslSocket::waitForEncrypted(int msecs = 30000)

К моему настоящему позору, я не заметил этого раньше...:(
Определенно нужно купить очки (к сожалению, это не шутка!)
Я готов изменить свой код соответствующим образом, чтобы протестировать его (в понедельник в офисе).
Скорее всего, это сработает. Что вы скажете: это сделает работу?
Да, странно отвечать на мой собственный вопрос, но, возможно, это решение, поэтому я решил поделиться:)

QSignalSpy — самый чистый способ

Чтобы узнать, как вы можете посмотреть это https://doc.qt.io/qt-6/qsignalspy.html .

Другие вопросы по тегам