如何不错过一个信号



我有这个代码,写在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();
}

按预期工作,但我想知道,是否有错过QNetworkReply发出的信号的风险?

我测试了如果在getconnect之间有一些延迟(使用QThread::sleep(1);),则不调用replyFinished

是否有方法要求对象在错过信号时重新发送信号?

我不得不不同意接受的答案:如果您删除QThread::sleep(1)(仅添加到';触发竞争条件"),那么if (reply->bytesAvailable() > 0)将始终返回false。它不提供任何防护。


首先,稍微绕一下Qt文档

一个更复杂的例子,假设经理已经存在,可以是:

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

和你一样,它们首先发送get请求,然后连接信号。
他们不会在文档中提供有bug的样本,这意味着它被认为对生产是安全的:没有机会丢失信号,因为在连接建立之前接收到数据。
这就是为什么if (reply->bytesAvailable() > 0)总是返回false的原因;网络的速度是它的千分之一。


如果您仍然想要小心,正确的方法是在之前将finished信号连接到您的插槽发送请求。这样,即使您错过了所有readyRead信号,您的插槽仍将至少被调用一次(因为之前已经建立了连接)。

我稍微调整了一下你的代码,使它可以编译并显示:

  1. if (reply->bytesAvailable() > 0)返回false(文本Call from if不显示)。
  2. 所有的应答都是从readyRead连接调用的。
  3. finished信号触发对replyFinished的最后一次调用;使用指向QNetworkReply的相同指针调用replyFinished
    由于我上面详细说明的原因(没有信号丢失,所以在第2点中所有内容都被消耗),该指针在最后调用中没有任何可读的内容。

调整后的代码如下:

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

QNetworkReply从另一个线程发送信号,因此这里确实可能存在竞争条件。

要防止它,可以这样做:

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

注意你的replyFinished可以被调用两次。

同样,readyRead并不表示整个回复已经到达,所以并不表示回复已完成。

我的理解是对QNetworkAccessManager::get的调用仅仅是"阶段"的请求:没有任何重要的事情发生,直到控制被传递回Qt事件循环。

考虑以下代码片段…

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

使用以下dummy_delay实现…

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

信号发出时,lambda被正确调用:即,尽管有延迟,但没有信号丢失(无论延迟持续时间如何—我测试了长达1分钟)。然而,对于下一个实现…

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

控件返回到事件循环,并且——假设下载时间少于10秒——finished信号"丢失"并且lambda从未被调用。

因此,在get请求之后调用connect是安全的,只要中间代码在任何时候都不将控制返回给事件循环。

[注意,以上是纯粹的经验,因为我还没有机会研究相关的Qt源代码]