0

I need some help understanding the details of accessing QObject subclasses from other threads.

From Qt documentation:

"If you are calling a function on an QObject subclass that doesn't live in the current thread and the object might receive events, you must protect all access to your QObject subclass's internal data with a mutex; otherwise, you may experience crashes or other undesired behavior."

Consider the following classes:

// Created on stack in main() => thread affinity = main thread
class Model : public QObject {
    Q_OBJECT
public:
    int getValue() const {
        return m_value;
    }
signals:
    void error(QString err);
public slots:
    void incrementValue() { 
        m_value++; 
    };
    void threadError(QString err) {
        emit error(err);
    }
private:
    std::atomic<int> m_value = 0;
}


class Thread : public QThread {
    Q_OBJECT
public:
    Thread(Model* model) : QThread(), m_model(model) {}
signals:
    void incrementValue();
    void error(QString err);
private:
    void run() override; // calls m_model->getValue(), emits incrementValue()
    Model* m_model;
};

Also assume that there is a Widget class with a QPushButton that when clicked calls the incrementValue() slot. Thread also occasionally emits the incrementValue signal and this is connected to the incrementValue() slot.

There is a potential data race: incrementValue() may be called simultaneously from the main thread by the user clicking the QPushButton as getValue() from a non main thread by Thread::run(). However since the m_value variable is atomic this is safe?

I guess it comes down to what constitutes the "QObject subclass's internal data". In my example m_value clearly is internal data. What else? Does the Q_OBJECT macro expand to variables that I need to mutex protect?

There is nothing in the above that is specific to QObject. In other words I have the same potential problems with multiple threads as if Model did not inherit from QObject. Qt does not fix my multithread problems in this case for me, but do not either introduce any new problems. Is this correct?

To simplify the discussion: both Model and Thread are as pure C++ as possible. I do not use QTimer, QTcpSocket, event loop inside Thread or anything else Qt. Nor do I call moveToThread. The only thing I use Qt for in this context is its signal/slot mechanism (and QString).

One possible, but cumbersome, solution that I see is: rewrite Model to NOT subclass QObject. Introduce:

class Gui2Model : public QObject {
Q_OBJECT
public:
   Gui2Model(Model* model) : m_model(model) {}
signals:
   void error(QString err);
public slots:
   void incrementValue() {
      m_model->incrementValue();
   }
   void errorSlot(QString err) {
     emit error(err);
   }
private:
   Model* m_model;
};

Connect the increment QPushButton clicked signal to the Gui2Model::incrementValue() slot.

Signal errors from Thread by calling:

void Model::error(QString err) {
    QMetaObject::invokeMethod(m_gui2model, "errorSlot", Qt::QueuedConnection, Q_ARG(QString, err));
}

That way I take the full burden of synchronizing Model, but at least it happens according to rules which I (should) know from standard C++.

Andy
  • 3,251
  • 4
  • 32
  • 53
  • 2
    When you use Qt use [signals and slots](https://doc.qt.io/qt-5/signalsandslots.html) to transfer data and information between threads. – chehrlic Feb 08 '22 at 16:43
  • 1
    Multithreading in Qt can get messed up if not implemented correctly. Recommended reading: [1](https://doc.qt.io/qt-5/threads-qobject.html), [2](https://wiki.qt.io/Threads_Events_QObjects), [3](https://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/), [4](https://www.qt.io/blog/2010/06/17/youre-doing-it-wrong), [5](https://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html), [6](https://www.kdab.com/the-eight-rules-of-multithreaded-qt/) – Arun Kumar B Feb 09 '22 at 10:49
  • @chehrlic: but how can I can call Model::getValue() from Thread::run() using signal/slots? – Andy Feb 09 '22 at 12:05

1 Answers1

1

Is it sufficient to rewrite the m_value variable to be std::atomic?

Assuming you join the thread before the model is destroyed, I think yes.

There is nothing in the above that is specific to QObject.

I think they wanted to stress "the object might receive events" part, which may not be a direct call in your code, but a signal/slot connection. And signal/slot connections would be specific to QObject.

In a very twisted scenario, you could have both a signal from you thread and a direct call from your thread, which would create a race condition :-)

As a rule it may perhaps be OK to pass const Model* to the Thread object. It should be safe to call const methods on the Model since these do not change the state of the Model?

If the model is created const, then yes. Actually, there wouldn't be much choice in such case. But if the model is created non-const, and the value could change from the main thread while accessing it from your thread, then the access to it would still need to be synchronized.

danadam
  • 3,350
  • 20
  • 18
  • Is the reading of the m_value not atomic? If it is being written to at the same time can I possibly read some of the bits from the old value and some from the new? If so I guess I need to lock a mutex inside every method of Model? Alternatively it may be easier to access Model trough a thread safe smart pointer? – Andy Feb 08 '22 at 21:12
  • 1
    It may or may not be, but in any case, from standard point of view it is undefined behavior (see [Threads and data races](https://en.cppreference.com/w/cpp/language/memory_model#Threads_and_data_races) on cppreference and also [Is it possible to read half-written ...](https://stackoverflow.com/questions/56780384/is-it-possible-to-read-half-written-corrupt-primitive-variable-when-using-multi) here on SO). – danadam Feb 08 '22 at 21:43
  • Not sure what you mean by "thread safe smart pointer". If you mean [std::atomic>](https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic2) then I don't think it will help with anything, because the race is on the data, not on the pointer. – danadam Feb 08 '22 at 21:44
  • I was thinking maybe making something like std::weak_ptr where I would have to call lock (but in this case it would lock a mutex) before getting another wrapper class that I could treat like a pointer. And when the wrapper class went out of function scope it would release the mutex. That way I would be certain that I always locked the mutex inside every function. – Andy Feb 09 '22 at 08:09
  • @Andy no need for smart pointers to have something like this. It can be achieved by having `std::mutex` in your model, a member function that returns a reference to it and then instead of calling lock you would create `std::lock_guard`. – danadam Feb 11 '22 at 01:26