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

Rei*_*ica 6 c++ qt thread-safety qthread

假设我们在QObject-deriving类中编写了一个非const方法:

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

我们希望使该方法成为线程安全的:意味着从任意线程调用它,并且同时从多个线程调用它,不应该引入未定义的行为.

  1. Qt提供了哪些机制/ API来帮助我们使该方法具有线程安全性?

  2. Qt的哪些机制/ API可以在方法执行"其他事情"时使用?

  3. 是否可以对"其他事物"进行分类,以便为使用Qt特定的机制/ API提供信息?

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

Rei*_*ica 9

适用的Qt API取决于线程安全方法的功能.让我们从最一般到最具体的情况来涵盖这些情况.

信号

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

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

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

在对象的线程中完成工作

最通用的方法是确保方法始终在对象中执行thread().这使得它对于对象具有线程安全性,但是当然也必须在线程安全地使用方法中的任何其他对象.

通常,只能从对象中调用线程不安全的方法thread():

void MyObject::method() {
  Q_ASSERT(thread() == QThread::currentThread());
  ...
}
Run Code Online (Sandbox Code Playgroud)

无线对象的特殊情况需要一些小心.一个对象在其线程完成时变为无线程.然而,仅仅因为该对象是无线程的,并不能使其所有方法都是线程安全的.为了线程安全的目的,最好选择一个线程来"拥有"这些对象.这样的线程可能是主线程:

Q_ASSERT(QThread::currentThread() == (thread() ? thread() : qApp()->thread()));
Run Code Online (Sandbox Code Playgroud)

我们的工作是履行这一主张.这是如何做:

  1. 利用线程安全信号.

    由于信号是线程安全的,我们可以将我们的方法作为信号,并在槽中承载它的实现:

    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);
      }
    };
    
    Run Code Online (Sandbox Code Playgroud)

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

  2. 将函数调用函数发送到对象的线程.

    很多方法可以做到这一点 ; 让我们提出一个执行动态分配最小数量的动态分配:在大多数情况下,只有一个动态分配.

    我们可以在仿函数中包装方法的调用,并确保它是线程安全的:

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

    如果当前线程是对象的线程,则没有开销和数据复制.否则,调用将被延迟到对象线程中的事件循环,或者如果对象是无线程的,则延迟到主事件循环.

    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)));
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 调用对象线程的调用.

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

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

    然后:

    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)...});
    }
    
    Run Code Online (Sandbox Code Playgroud)

保护对象的数据

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

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;
  };
};
Run Code Online (Sandbox Code Playgroud)

在对象的线程中使用特定于对象的互斥锁和调用方法体之间的选择取决于应用程序的需要.如果在方法中访问的所有成员都是私有的,那么使用互斥锁是有意义的,因为我们处于控制之下并且可以通过设计确保所有访问都受到保护.使用特定于对象的互斥锁也会将该方法与对象事件循环的争用分离 - 因此可能会带来性能优势.另一方面,该方法必须在它不拥有的对象上访问线程不安全的方法,然后互斥量不足,并且该方法的主体应该在对象的线程中执行.

读一个简单的成员变量

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

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

修改简单成员变量

如果该方法修改数据的一个单件,可以被包裹在QAtomicIntegerQAtomicPointer,并且可以使用原子原语来完成该操作,我们可以使用一个原子字段:

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

这种方法一般不会扩展到修改多个成员:一些成员被更改而其他成员不被更改的中间状态将对其他线程可见.通常这会破坏其他代码所依赖的不变量.