QLocalSocket+QEventLoopを組み合わせた時のreadyReadシグナルの仕様

QLocalSocketとQEventLoopを組み合わせると、非同期なネットワーク呼び出しを同期的に記述できます。(もちろんUIスレッドはブロックしません)。

詳細はここの”Forcing event dispatching”という箇所を参考にして下さい。
http://qt-project.org/wiki/Threads_Events_QObjects

基本的な流れとしてはQLocalSocketにデータを何かwriteした後ローカルのQEventLoopを呼び出し(exec()というメソッド)、readyReadシグナルが届いたらイベントループを終了(quit()メソッド)させるという流れです。

しかしここで大きな落とし穴があり、readyReadシグナルが呼ばれた中で再びイベントループを呼び出すと、readyReadシグナルはそのイベントループでは呼び出されません。Qtのドキュメントにもそのように書かれています。(僕はこの仕様でかなりはまりました^^;)

http://doc.qt.io/qt-5/qiodevice.html#readyRead

readyRead() is not emitted recursively; if you reenter the event loop or call waitForReadyRead() inside a slot connected to the readyRead() signal, the signal will not be reemitted (although waitForReadyRead() may still return true).

実際のソースコードの該当箇所を見てみると以下のようになっています。

qabstractsocket.cpp

// only emit readyRead() when not recursing, and only if there is data available
bool hasData = newBytes > 0
#ifndef QT_NO_UDPSOCKET
|| (!isBuffered && socketType != QAbstractSocket::TcpSocket && socketEngine && socketEngine->hasPendingDatagrams())
#endif
|| (!isBuffered && socketType == QAbstractSocket::TcpSocket && socketEngine)
;

if (!emittedReadyRead && hasData) {
QScopedValueRollback r(emittedReadyRead);
emittedReadyRead = true;
emit q->readyRead();
}

readyReadシグナルにDirectConnectionを使ってconnectされたスロットが呼び出され(この時emittedReadyRead == trueとなる)、その中でイベントループが作られ再度上記の箇所に到達した場合、emittedReadyReadはすでにtrueなので再度emitされないという仕組みです。イベントループが終了するとQScopedValueRollbackによりemittedReadyReadの値がfalseに戻され、再びemitされるようになります。

この仕様を実験するため以下のコードを書きました。
QLocalServer、QLocalSocketを使ってサーバ、クライアント間でUnix Domain Socketを介してデータを相互に送受信しています。
動作確認した環境はMac (Mavericks) , Qt 5.4です。
readyReadシグナルにconnectされたonReadyReadスロットの中でQEvnetLoopを作成、実行しています。

サーバ側のコード

Server.h

#pragma once

#include
#include
#include
#include
#include

class Result : public QObject {
Q_OBJECT

int m_result;

public:
int result() { return m_result; }
void setResult(int result) {
m_result = result;
emit ready();
}

signals:
void ready();
};

class Server : public QObject {
Q_OBJECT

Result m_result1;
Result m_result3;
QLocalSocket* m_sock;

public:
void start() {
QString sockPath = "/tmp/qeventloop_test";
QLocalServer* server = new QLocalServer();
QFile sockFile(sockPath);
if (sockFile.exists()) {
sockFile.remove();
}
server->listen(sockPath);
connect(server, &QLocalServer::newConnection, [this, server]() {
m_sock = server->nextPendingConnection();
connect(m_sock, &QLocalSocket::disconnected, m_sock, &QLocalSocket::deleteLater);
QObject::connect(
m_sock, &QLocalSocket::readyRead, this, &Server::onReadyRead, Qt::QueuedConnection);

sendData(m_sock, 1);

QEventLoop loop;
connect(&m_result1, &Result::ready, &loop, &QEventLoop::quit);
qDebug("start event loop to wait for 1");
loop.exec();
qDebug("end event loop to wait for 1");
});
}

void onReadyRead() {
qDebug("bytesAvailable: %lld", m_sock->bytesAvailable());
qint64 bytesAvailable = m_sock->bytesAvailable();
QByteArray buffer = m_sock->readAll();
QDataStream ds(buffer);
while (bytesAvailable > 0) {
int num;
ds >> num;
qDebug("received %d", num);
bytesAvailable -= 4;
if (num == 2) {
sendData(m_sock, 3);

QEventLoop loop;
QObject::connect(&m_result3, &Result::ready, &loop, &QEventLoop::quit);
qDebug("start event loop to wait for 3");
loop.exec();
qDebug("end event loop to wait for 3");

} else if (num == -1) {
m_result1.setResult(num);
} else if (num == -3) {
m_result3.setResult(num);
}
}
}

void sendData(QLocalSocket* sock, int num) {
qDebug("send %d", num);
QByteArray block;
QDataStream ds(&block, QIODevice::WriteOnly);
ds << num;
sock->write(block);
}
};

main.cpp

#include 
#include
#include
#include "Server.h"

int main(int argv, char** args) {
QApplication app(argv, args);

Server server;
server.start();

return app.exec();
}

クライアント側のコード

main.cpp

#include 
#include
#include

void sendData(QLocalSocket& sock, int num) {
qDebug("send %d", num);
QByteArray block;
QDataStream ds(&block, QIODevice::WriteOnly);
ds << num;
sock.write(block);
}

int main(int argv, char** args) {
QApplication app(argv, args);

QLocalSocket sock;
QObject::connect(&sock, &QLocalSocket::readyRead, [&sock]() {
qint64 bytesAvailable = sock.bytesAvailable();
QByteArray buffer = sock.readAll();
QDataStream ds(buffer);
while (bytesAvailable > 0) {
int num;
ds >> num;

qDebug("received %d", num);
bytesAvailable -= 4;

if (num == 1) {
sendData(sock, 2);
sendData(sock, -1);
} else if (num == 3) {
sendData(sock, -3);
}
}
});

sock.connectToServer("/tmp/qeventloop_test");
return app.exec();
}

実行結果は以下のようになります。

send 1
start event loop to wait for 1
bytesAvailable: 8
received 2
send 3
start event loop to wait for 3
bytesAvailable: 4
received -3
end event loop to wait for 3
received -1
end event loop to wait for 1

次にサーバ側プログラムの以下のQueuedConnectionとなっている箇所を

QObject::connect(m_sock, &QLocalSocket::readyRead, this, &Server::onReadyRead, Qt::QueuedConnection);

以下のようにDirectConnection(デフォルトではsenderとreceiverが同一スレッドであればDirectConnectionとなります)を使うように変更してみます。

      QObject::connect(
m_sock, &QLocalSocket::readyRead, this, &Server::onReadyRead);

実行結果は以下のようになります。クライアントが-3を送信してもサーバのQLocalSocketはreadyReadシグナルが呼び出されないので、以下のように永遠にイベントループが終了しません。

send 1
start event loop to wait for 1
bytesAvailable: 8
received 2
send 3
start event loop to wait for 3

QLocalSocket, QEventLoopを使う時はreadyReadシグナルの仕様に気をつけましょう!