c++multithreadingqtsocketsqsslsocket

Why am I getting runtime errors, with Sockets (TCP/Web) when moved to another thread? Is `QObject::moveToThread()` a synchronous call?


I am moving some sockets from main thread to worker thread and processing readyRead(), close(), write() etc. on the new thread. Rarely I see below dangerous warning:

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

Usually the execution after this warning results in undefined behaviour (crash / uncaught exception / normal).
Have checked all the signal/slot to the socket and they seem proper. From my experience, usually the above warning will be frequent or quick if there is any coding irregularity.

Now I am suspecting if moveToThread() does its job as expected or not! Because from QEvent documentation a QEvent::ThreadChange is raised as the last event on the current thread during this function call.

The object is moved to another thread. This is the last event sent to this object in the previous thread. See QObject::moveToThread()1.
1 A QEvent::ThreadChange event is sent to this object just before the thread affinity is changed. You can handle this event to perform any special processing.

In my code, I am not checking for this event. Rather I assume that, once the moveToThread() is finished, the thread affinity is changed and the new "signal/slot" on the object is guaranteed on the new thread.

Question:

  1. Is it safe to move the sockets to another thread?
  2. Will moveToThread() assure that the signals on that object are also moved to the new thread just after this function?
  3. How should it be designed to assure no threading irregularity with Sockets?

BTW, in the latest Qt debugger it lands on following code segment:

// qsocketnotifier.cpp
if (Q_UNLIKELY(thread() != QThread::currentThread())) {
    qWarning("QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread");
    return;
}

Below is the stack frame for the main thread: enter image description here

Below is the stack frame for the worker thread, which halts on the logging: enter image description here


Solution

  • Qt sockets are NOT supported with moveToThread() idiom!!!

    I wish Qt had documented this fact as LOUD as above. I have spent weeks of debugging an issue which is not meant for fixing. Socket moveToThread() issue is so complicated, that it doesn't happen consistently. I could reproduce it consistently only after testing a customised QTcpServer subclass with lots of socket open/close scenario.

    Later I came across this post: How do I execute QTcpSocket in a different thread?. This answer explicitly states that sockets are not supported for the movement across the threads.

    Here are the documentations from QTcpServer

    Note: The returned QTcpSocket object cannot be used from another thread. If you want to use an incoming connection from another thread, you need to override incomingConnection().

    Note: If you want to handle an incoming connection as a new QTcpSocket object in another thread you have to pass the "socketDescriptor" to the other thread and create the QTcpSocket object there and use its setSocketDescriptor() method.

    Probably this will apply to the QWebSocketServer too!


    So the best approach is to overload incomingConnection() and call its internal in another thread.

    void MyTcpServer::incomingConnection (qintptr socketDescriptor) override
    {
      emit SignalToDifferentThread(socketDescriptor);
    }
    

    If done so, the addPendingConnection() and nextPendingConnection() idiom may not be required, even though it's mentioned in a note.

    Note: If another socket is created in the reimplementation of this method, it needs to be added to the Pending Connections mechanism by calling addPendingConnection(). (<-- this may not apply for the sockets in different thread)


    QWebSocketServer is stricter than QTcpServer

    This is another fact, which wasted more time for me. In QTcpServer, we can overload the incomingConnection() and pass on the socket descriptor to another thread to create a socket. However, QWebSocketServer doesn't have such overload. Mostly because, QSslSocket received in QTcpServer gets upgraded to QWebSocket and passed on to the QWebSocketServer via its handleConnection() method.

    So QWebSocket must be created where QWebSocketServer resides!. Hence in whichever threads, we want a QWebSocket to be created, all those threads need to have an object of QWebSocketServer. Idiomatically we can have one QTcpServer listening on a single port and can keep passing its QTcpSockets (upgraded to QWebSockets) to these various threads' QWebSocketServers in a load balancing way.


    A message to Qt developers

    I wish that, you can save lot of time of the developers relying on the Qt library by a simple compilation error. This will negate all the wrong articles floating around internet which suggest how to use moveToThread() on a QTcpSocket. Just introduce below method:

    class QTcpServer / QTcpSocket / QWebSocketServer / QWebSocket : public QObject
    {
      Q_OBJECT
      ...
      // Create this object in a desired thread instead of moving
      private: void moveToThread (QThread*) = delete; // hiding `QObject::moveToThread()`
    };
    

    Update: Raised QTBUG-82373.