2015年3月27日金曜日

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<bool> 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 <QLocalServer>
#include <QLocalSocket>
#include <QFile>
#include <QEventLoop>
#include <QObject>

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 <QApplication>
#include <QDebug>
#include <QObject>
#include "Server.h"

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

  Server server;
  server.start();

  return app.exec();
}

クライアント側のコード

main.cpp

#include <QDebug>
#include <QLocalSocket>
#include <QApplication>

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シグナルの仕様に気をつけましょう!

0 件のコメント:

コメントを投稿