如何利用 Qt 使 QObject 方法线程安全?

2021-12-09 00:00:00 qt thread-safety qthread c++

假设我们在一个 QObject 派生类中编写了一个非常量方法:

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. Qt 提供哪些机制/API 来帮助我们使该方法成为线程安全的?

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

Qt 的哪些机制/API 在该方法还可以完成其他事情"时可以使用?

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

是否可以对其他事物"进行分类,以告知要使用哪些 Qt 特定机制/API?

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

题外话是 C++ 标准本身提供的机制,以及确保线程安全的通用/非 Qt 特定方法.

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

推荐答案

适用的 Qt API 取决于线程安全方法的功能是什么.让我们从最普遍到最具体的情况进行介绍.

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.

信号体由 moc 工具生成并且是线程安全的.

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

推论 1:所有直接连接的槽/函子必须是线程安全的:否则破坏信号契约.虽然信号槽系统允许代码解耦,但直接连接的特定情况会将信号的要求泄漏给连接的代码!

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!

推论 2:直接连接比自动连接更紧密.

最通用的方法是确保方法始终在对象的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.

一般来说,线程不安全的方法只能从对象的thread()中调用:

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. 利用线程安全信号.

  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);
  }
};

这种方法可以支持断言,但很冗长,并且每个参数都执行额外的动态分配(至少从 Qt 5.7 开始).

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).

将函子中的调用分派给对象的线程.

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)));
}

  • 将调用分派到对象的线程.

  • Dispatch the call to the object's thread.

    这是上述的变体,但没有使用函子.postCall 函数可以显式包装参数:

    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;
    }
    

    那么:

    template <typename Class, typename... Args>
    struct CallEvent : public QEvent {
       // See https://stackoverflow.com/a/7858971/1329652
       // See also https://stackoverflow.com/a/15338881/1329652
       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)...});
    }
    

  • 保护对象的数据

    如果该方法对一组成员进行操作,则可以使用互斥锁来序列化对这些成员的访问.利用 QMutexLocker 表达您的意图,并通过构造避免未发布的互斥锁错误.

    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.

    如果 const 方法读取可以包装在 QAtomicIntegerQAtomicPointer 中的单个数据,我们可以使用原子字段:

    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();
      };
    };
    

    修改简单的成员变量

    如果该方法修改了可以包装在QAtomicIntegerQAtomicPointer 中的单个数据,则可以使用和原子原语,我们可以使用原子字段:

    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.

    相关文章