How not to miss a signal

128 Views Asked by At

I have this code, written in qt 5.15:

Widget::Widget()
{
    manager = new QNetworkAccessManager(this);
    QNetworkReply *reply = manager->get(QNetworkRequest(QUrl("http://example.com")));
    connect(reply, &QNetworkReply::readyRead, this, &Widget::replyFinished));

}
void Widget::replyFinished(QNetworkReply* reply)
{
    // some processing here
    qDebug() << reply.readAll();
}

It works as expected, but I wonder, is there a risk to miss the signal emmited by QNetworkReply?

I tested that if I had some delay (with QThread::sleep(1);) between the get and the connect, replyFinished is not called.

Is there a method to ask an object to resend a signal if it has been missed?

3

There are 3 best solutions below

2
hyde On

QNetworkReply sends the signal from another thread, so there is indeed a possible race condition here.

To guard against it, you can do this:

Widget::Widget()
{
    manager = new QNetworkAccessManager(this);
    QNetworkReply *reply = manager->get(QNetworkRequest(QUrl("http://example.com")));
    QThread::sleep(1); // trigger race condition
    connect(reply, &QNetworkReply::readyRead, this, &Widget::replyFinished));
    if (reply->bytesAvailable() > 0) replyFinished(reply);

}

Note that your replyFinished can get called twice.

Also,readyRead does not mean entire reply has arrived, so it does not mean "reply finished".

0
Atmo On

I have to disagree with the accepted answer: if you remove QThread::sleep(1) (that was added only to "trigger a race condition"), then the if (reply->bytesAvailable() > 0) will always return false. It provides no guard against anything.


First, a small detour to the Qt documentation

A more involved example, assuming the manager is already existent, can be:

 QNetworkRequest request;
 request.setUrl(QUrl("http://qt-project.org"));
 request.setRawHeader("User-Agent", "MyOwnBrowser 1.0");

 QNetworkReply *reply = manager->get(request);
 connect(reply, &QIODevice::readyRead, this, &MyClass::slotReadyRead);
 connect(reply, &QNetworkReply::errorOccurred,
         this, &MyClass::slotError);
 connect(reply, &QNetworkReply::sslErrors,
         this, &MyClass::slotSslErrors);

Like you, they first send a get request, then connect signals.
They would not provide a buggy sample in their doc, meaning it was deemed safe for production: no chance signals will be lost because data is received before the connection is established.
That is the very reason why if (reply->bytesAvailable() > 0) will always return false BTW; a network is just thousands of times too slow for it to be otherwise.


If you still want to be careful, the correct approach is to connect the finished signal to your slot before sending the request. That way, even if you miss all the readyRead signals, your slot will still be called at least once (since the connection was made before).

I have tweaked your code a bit to make it compile and show:

  1. The if (reply->bytesAvailable() > 0) returns false (text Call from if is never displayed).
  2. All the reply is called from the readyRead connection.
  3. The finished signal triggers the last call to replyFinished after everything is received; replyFinished is called using the same pointer to QNetworkReply as the rest.
    That pointer has, for the reasons I detailed above (no signal is lost so everything gets consummed in point 2), nothing to read on that final call.

Here is the tweaked code:

Widget::Widget()
{
    manager = new QNetworkAccessManager(this);
    QNetworkRequest request;
    request.setUrl(QUrl("http://www.google.com"));
//Safety is assured by making the following connection
//before any http request is sent.
    connect(manager, &QNetworkAccessManager::finished, this, &Widget::replyFinished);
    QNetworkReply* reply = manager->get(request);
    connect(reply, &QNetworkReply::readyRead, this, 
        [=]() { qDebug() << "Called from readyRead"; this->replyFinished(reply); }
    );
    if (reply->bytesAvailable() > 0) {
        qDebug() << "Call from if";
        replyFinished(reply);
    }
}
void Widget::replyFinished(QNetworkReply* reply)
{
    QString strReply = reply->readAll();
    qDebug() << (void*)reply << " read " << strReply.length() << " characters";
}
0
G.M. On

My understanding is that the call to QNetworkAccessManager::get merely 'stages' the request: nothing of importance occurs until control is passed back to the Qt event loop.

Consider the code snippet...

QNetworkAccessManager manager;
QNetworkReply *reply = manager.get(QNetworkRequest(QUrl("https://www.bbc.co.uk")));
dummy_delay();
QObject::connect(reply, &QNetworkReply::finished, qApp,
                 [&]()
                   {
                     std::log << "finished: error = " << reply->error() << '\n';
                     qApp->quit();
                   });

With the following implementation of dummy_delay...

void dummy_delay ()
{

    /*
     * Blocking (i.e. no events processed) sleep for 10 seconds.
     */
    QThread::sleep(10);
}

the lambda is called correctly when the finished signal is emitted: i.e. no signal is lost despite the delay (and regardless of the delay duration -- I tested up to 1 minute). With the next implementation however...

void dummy_delay ()
{

    /*
     * Process events for 10 seconds.
     */
    QEventLoop el;
    QTimer::singleShot(10000, [&]{ el.quit(); });
    el.exec();
}

control returns to the event loop and -- assuming the download takes less than 10s -- the finished signal is 'lost' and the lambda never called.

So it's safe to call connect after the get request provided the intervening code does not, at any point, return control to the event loop.

[Note that the above is purely empirical as I haven't had a chance to study the relevant Qt source code yet.]