覚え書きブログ

Qtの覚え書き(socketとmulti thread)

Qtで、マルチスレッドでソケットプログラミングをしていたら、次のようなエラーがでた。解決方法をメモっておく。

QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread

今回は、次のようにDialog.cppのon_startPUshButton_clicked()にて、QTcpSocketのソケット(tcpSocket)を作りconnectToHostでサーバに接続した後、startJoyThreadでスレッドを開始している。その際に、tcpSocketをアドレス渡している。

void Dialog::on_startPushButton_clicked()
{
    //----------------------
    // Socket
    QTcpSocket *tcpSocket = new QTcpSocket;
    tcpSocket->connectToHost(ipAddress,tcpPort);
    tcpSocket->waitForConnected();
    //----------------------

    //----------------------
    // Thread
    // stop thread
    if(myJoyThread.isRunning())
        myJoyThread.stop();

    // start 
    myJoyThread.startJoyThread(tcpSocket);
    //----------------------

    qDebug("%d",ui->joyComboBox->currentIndex());
}

スレッド側では、run()にてDialogから受け取ったソケット(tcpSocket)に、writeでメッセージを送っている。

void JoyThread::startJoyThread(QTcpSocket *tcpSocket)
{
    qDebug()<<"startJoyThread: "<<currentThreadId();

    this->tcpSocket = tcpSocket;

    // stop thread
    if(isRunning())
        stop();

    // start thread
    start();
    runFlag = true;
}

void JoyThread::run()
{
    qDebug()<<"run: "<<currentThreadId();
    while(runFlag) // loop if runFlag on
    {
       // make command
       QByteArray sendMsg = QString("%1:%2:%3").arg(cmd).arg(id).arg(val).toUtf8();

       // send command to server through socket "tcpSocket"
       tcpSocket->write(sendMsg);
    }
}

実行結果は以下のようになる。「startJoyThread: 0x1e44」と「run: 0x5c0」は、currentThreadId()の出力でスレッドIDである。

startJoyThread:  0x1e44
run:  0x5c0
QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread
QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread

このstartJoyThreadとrunのスレッドIDが0x1e44と0x5c0と異なることからわかるように、Dialog::on_startPushButton_clicked()とJoyThread::run()は異なるスレッドで動いている。

具体的には、下記に説明されているように、QtのGUI(すべてのwidgetと関連するクラス)はメインスレッド(またはGUIスレッド)で動いていて、JoyThread::run()のように新たに生成されたスレッドは、メインとは別のワーカースレッドで動いている。つまり、上記の場合、一つのソケット(tcpSocket)に異なる2つのスレッドからアクセスしているため、「QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread」
というエラーがでていたのだ。

This thread is called the "main thread" (also known as the "GUI thread" in Qt applications). The Qt GUI must run in this thread. All widgets and several related classes, for example QPixmap, don't work in secondary threads. A secondary thread is commonly referred to as a "worker thread" because it is used to offload processing work from the main thread.

http://doc.qt.io/qt-4.8/thread-basics.html

これを解決するためには、「moveToThread」を用いて、ソケットの所有者をメインスレッドから、実際に通信を行うワーカースレッドmyJoyThread(JoyThread)に変えてやればよい。

void Dialog::on_joyStartPushButton_clicked()
{
    //----------------------
    // Socket
    tcpSocket = new QTcpSocket;
    tcpSocket->connectToHost(ipAddress,tcpPort);
    tcpSocket->waitForConnected();
    <span style="color: #ff5252">tcpSocket->moveToThread(&myJoyThread); //tcpSocket in main thread is moved to JoyThread (could be accessed from run())</span>
    //----------------------

    //----------------------
    // Thread
    // stop joystick thread
    if(myJoyThread.isRunning())
        myJoyThread.stop();

    // start joystick thread with joystick id
    myJoyThread.startJoyThread(ui->joyComboBox->currentIndex(),tcpSocket);
    //----------------------
}