web-dev-qa-db-fra.com

Qt5: Comment attendre un signal dans un thread?

La question du titre n'est probablement pas très explicite. J'utilise Qt5 sur Windows7.

Dans un thread (QThread) à un moment donné, dans la fonction/méthode "process()", je dois attendre le SIGNAL "encrypted()" appartenant à un QSslSocket que j'utilise dans ce thread. Je suppose également que je devrais utiliser un QTimer et attendre un "timeout()" SIGNAL afin d'éviter d'être bloqué dans une boucle infinie ...
Ce que j'ai maintenant c'est:

// 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;
}

Eh bien, évidemment, cela ne fonctionne pas. Il reste dans cette boucle while pour toujours ...
La question est donc la suivante: existe-t-il un moyen de résoudre ce problème? Comment puis-je attendre cette "encrypted()" SIGNAL mais pas plus - disons 10 secondes - afin d'éviter de rester coincé dans cette boucle/thread d'attente?

15

Vous pouvez utiliser une boucle d'événement locale pour attendre l'émission du signal:

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

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

Ici, il attend que encrypted soit émis ou que le délai expire.

34
Nejat

En programmation asynchrone, "l'attente" est considérée comme un anti-modèle. Au lieu d'attendre des choses, concevez le code pour réagir à une condition qui se réalise. Par exemple, connectez le code à un signal.

Une façon de l'implémenter consiste à découper vos actions en états distincts et à effectuer certains travaux lorsque chacun des états est entré. Bien sûr, si la quantité de travail n'est pas anodine, utilisez un emplacement séparé au lieu d'un lambda pour garder les choses lisibles.

Notez l'absence de gestion explicite de la mémoire. L'utilisation de pointeurs propriétaires vers des classes Qt est une optimisation prématurée et doit être évitée lorsque cela n'est pas nécessaire. Les objets peuvent être des membres directs du Worker (ou de son PIMPL ).

Les sous-objets doivent tous faire partie de la hiérarchie de propriété qui a Worker à la racine. De cette façon, vous pouvez déplacer en toute sécurité l'instance Worker vers un autre thread, et les objets qu'elle utilise la suivront. Bien sûr, vous pouvez également instancier le Worker dans le bon thread - il y a un simple idiom pour cela. Le répartiteur d'événements du thread possède le travailleur, donc lorsque la boucle d'événements du thread se ferme (c'est-à-dire après avoir appelé QThread::quit()), le travailleur sera automatiquement supprimé et aucune ressource ne fuira.

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);
  });
}

La mise en œuvre du travailleur:

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();
  }
};

Alors:

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();
}

Notez que la connexion à QThread::started() serait racée: le répartiteur d'événements n'existe pas tant que du code dans QThread::run() n'a pas eu la chance de s'exécuter. Ainsi, nous devons attendre que le thread y arrive en cédant - il est très probable que le thread de travail progresse suffisamment loin en un ou deux rendements. Ainsi, cela ne perdra pas beaucoup de temps.

7
Kuba Ober

J'ai eu du temps ces jours-ci et j'ai fait une enquête ...
Eh bien, j'ai parcouru " http://doc.qt.io/qt-5/qsslsocket.html " et j'ai trouvé ceci:

bool QSslSocket::waitForEncrypted(int msecs = 30000)

À ma honte, je ne l'avais pas remarqué avant ... :(
Certainement besoin d'acheter des lunettes ( malheureusement , ce n'est pas une blague!)
Je suis disposé à modifier mon code en conséquence afin de le tester (lundi @ office).
Probablement que cela fonctionnera. Que dites-vous: cela ferait-il le travail?
Oui, un peu bizarre de répondre à ma propre question, mais peut-être que c'est IS une solution, alors j'ai décidé de partager :)

3