How to leverage Qt to make a QObject method thread-safe?
Asked Answered
P

1

7

Suppose we wrote a non-const method in a QObject-deriving class:

class MyClass : public QObject {
  int x;
public:
  void method(int a) {
    x = a; // and possibly other things
  };
};

We want to make that method thread-safe: meaning that calling it from an arbitrary thread, and from multiple threads concurrently, shouldn't introduce undefined behavior.

  1. What mechanisms/APIs does Qt provide to help us make that method thread-safe?

  2. What mechanisms/APIs from Qt one could use when the method does the "other things" too?

  3. Is there any classification possible of the "other things" that would inform what Qt-specific mechanisms/APIs to use?

Off topic are mechanisms provided by the C++ standard itself, and generic/non-Qt-specific ways of ensuring thread-safety.

Polliwog answered 2/11, 2016 at 14:44 Comment(0)
P
11

The applicable Qt APIs depend on what is the functionality of the thread-safe method. Let's cover the circumstances from the most general to most specific.

Signals

The bodies of signals are generated by the moc tool and are thread-safe.

Corollary 1: All directly-connected slots/functors must be thread-safe: doing otherwise breaks the contract of a signal. While the signal-slot system allows decoupling of code, the specific case of a direct connection leaks the requirements of a signal to the connected code!

Corollary 2: Direct connections couple tighter than automatic connections.

Doing the Work in the Object's Thread

The most general approach is that of ensuring that the method's is always executed in the object's thread(). This makes it thread-safe in respect to the object, but of course the use of any other objects from within the method must be done thread-safely too.

In general, a thread-unsafe method can only be called from the object's thread():

void MyObject::method() {
  Q_ASSERT(thread() == QThread::currentThread());
  ...
}

The special case of a thread-less object requires some care. An object becomes thread-less when its thread finishes. Yet, just because the object is thread-less doesn't make all of its methods thread-safe. It would be preferable to choose one thread to "own" such objects for the purpose of thread-safety. Such thread might be the main thread:

Q_ASSERT(QThread::currentThread() == (thread() ? thread() : qApp()->thread()));

Our job is to fulfill that assertion. Here's how:

  1. Leverage thread-safe signals.

    Since signals are thread-safe, we could make our method a signal, and host its implementation in a slot:

    class MyObject : public QObject {
      Q_OBJECT
      int x;
      void method_impl(int a) {
        x = a;
      }
      Q_SIGNAL void method_signal(int);
    public:
      void method(int a) { method_signal(a); }
      MyObject(QObject * parent = nullptr) : QObject{parent} {
        connect(this, &MyObject::method, this, &MyObject::method_impl);
      }
    };
    

    This approach works to uphold the assertion, but is verbose and performs an additional dynamic allocation per each argument (as of Qt 5.7 at least).

  2. Dispatch the call in a functor to the object's thread.

    There are many ways of doing it; let's present one that does the minimum number of dynamic allocations: in most cases, exactly one.

    We can wrap the call of the method in a functor and ensure that it's executed thread-safely:

    void method1(int val) {
       if (!isSafe(this))
          return postCall(this, [=]{ method1(val); });
       qDebug() << __FUNCTION__;
       num = val;
    }
    

    There is no overhead and no copying of data if the current thread is the object's thread. Otherwise, the call will be deferred to the event loop in the object's thread, or to the main event loop if the object is threadless.

    bool isSafe(QObject * obj) {
       Q_ASSERT(obj->thread() || qApp && qApp->thread() == QThread::currentThread());
       auto thread = obj->thread() ? obj->thread() : qApp->thread();
       return thread == QThread::currentThread();
    }
    
    template <typename Fun> void postCall(QObject * obj, Fun && fun) {
       qDebug() << __FUNCTION__;
       struct Event : public QEvent {
          using F = typename std::decay<Fun>::type;
          F fun;
          Event(F && fun) : QEvent(QEvent::None), fun(std::move(fun)) {}
          Event(const F & fun) : QEvent(QEvent::None), fun(fun) {}
          ~Event() { fun(); }
       };
       QCoreApplication::postEvent(
                obj->thread() ? obj : qApp, new Event(std::forward<Fun>(fun)));
    }
    
  3. Dispatch the call to the object's thread.

    This is a variation on the above, but without using a functor. The postCall function can wrap the parameters explicitly:

    void method2(const QString &val) {
       if (!isSafe(this))
          return postCall(this, &Class::method2, val);
       qDebug() << __FUNCTION__;
       str = val;
    }
    

    Then:

    template <typename Class, typename... Args>
    struct CallEvent : public QEvent {
       // See https://mcmap.net/q/16874/-quot-unpacking-quot-a-tuple-to-call-a-matching-function-pointer
       // See also https://mcmap.net/q/905150/-use-std-tuple-for-template-parameter-list-instead-of-list-of-types
       template <int ...> struct seq {};
       template <int N, int... S> struct gens { using type = typename gens<N-1, N-1, S...>::type; };
       template <int ...S>        struct gens<0, S...> { using type = seq<S...>; };
       template <int ...S>        void callFunc(seq<S...>) { (obj->*method)(std::get<S>(args)...); }
       Class * obj;
       void (Class::*method)(Args...);
       std::tuple<typename std::decay<Args>::type...> args;
       CallEvent(Class * obj, void (Class::*method)(Args...), Args&&... args) :
          QEvent(QEvent::None), obj(obj), method(method), args(std::move<Args>(args)...) {}
       ~CallEvent() { callFunc(typename gens<sizeof...(Args)>::type()); }
    };
    
    template <typename Class, typename... Args> void postCall(Class * obj, void (Class::*method)(Args...), Args&& ...args) {
       qDebug() << __FUNCTION__;
       QCoreApplication::postEvent(
                obj->thread() ? static_cast<QObject*>(obj) : qApp, new CallEvent<Class, Args...>{obj, method, std::forward<Args>(args)...});
    }
    

Protecting the Object's Data

If the method operates on a set of members, the access to these members can be serialized by using a mutex. Leverage QMutexLocker to express your intent and avoid unreleased mutex errors by construction.

class MyClass : public QObject {
  Q_OBJECT
  QMutex m_mutex;
  int m_a;
  int m_b;
public:
  void method(int a, int b) {
    QMutexLocker lock{&m_mutex};
    m_a = a;
    m_b = b;
  };
};

The choice between using an object-specific mutex and invoking the body of the method in the object's thread depends on the needs of the application. If all of the members accessed in the method are private then using a mutex makes sense since we're in control and can ensure, by design, that all access is protected. The use of object-specific mutex also decouples the method from the contention on the object's event loop - so might have performance benefits. On the other hand, is the method must access thread-unsafe methods on objects it doesn't own, then a mutex would be insufficient, and the method's body should be executed in the object's thread.

Reading a Simple Member Variable

If the const method reads a single piece of data that can be wrapped in a QAtomicInteger or QAtomicPointer, we can use an atomic field:

class MyClass : public QObject {
  QAtomicInteger<int> x;
public:
  /// Thread-Safe
  int method() const {
    return x.load();
  };
};

Modifying a Simple Member Variable

If the method modifies a single piece of data that can be wrapped in QAtomicInteger or QAtomicPointer, and the operation can be done using an atomic primitive, we can use an atomic field:

class MyClass : public QObject {
  QAtomicInteger<int> x;
public:
  /// Thread-Safe
  void method(int a) {
    x.fetchAndStoreOrdered(a);
  };
};

This approach doesn't extend to modifying multiple members in general: the intermediate states where some members are changed and some other are not will be visible to other threads. Usually this would break invariants that other code depends on.

Polliwog answered 2/11, 2016 at 14:44 Comment(1)
Could you please provide an example for methods that return some value (not void)?Oversee

© 2022 - 2024 — McMap. All rights reserved.