如何利用Qt使QObject方法成为线程安全的? [英] How to leverage Qt to make a QObject method thread-safe?

查看:169
本文介绍了如何利用Qt使QObject方法成为线程安全的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我们在 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来帮助我们使该方法成为线程-safe?

  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?

Off主题是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方法读取的单个数据可以包装在 QAtomicInteger QAtomicPointer ,我们可以使用原子字段:

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



    修改简单成员变量



    如果该方法修改一条数据,该数据可以包装在 QAtomicInteger QAtomicPointer 该操作可以使用原子基元完成,我们可以使用原子字段:

    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.

    这篇关于如何利用Qt使QObject方法成为线程安全的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆