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

7
假设我们在一个派生自 QObject 的类中编写了一个非 const 方法:
class MyClass : public QObject {
  int x;
public:
  void method(int a) {
    x = a; // and possibly other things
  };
};

我们希望使这种方法变得线程安全:也就是说,从任意线程调用它,以及同时从多个线程调用它,都不应该引入未定义的行为。
  1. Qt提供哪些机制/API可以帮助我们使该方法线程安全?

  2. 在该方法执行其他任务时,可以使用Qt的哪些机制/API?

  3. 是否可以对“其他任务”进行分类,以确定使用哪些Qt特定的机制/API?

C ++标准本身提供的机制和确保线程安全的通用/非Qt特定方式属于离题内容。

1个回答

11

根据方法的功能,适用的Qt API因情况而异。让我们从最一般到最具体地介绍一下。

信号

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

推论1:所有直接连接的槽或函数对象必须是线程安全的:否则违反了信号的契约。尽管信号槽系统允许代码解耦,但直接连接的特定情况会使信号的要求泄漏到连接的代码中!

推论2:直接连接比自动连接更紧密地耦合。

在对象的线程中执行操作

最通用的方法是确保该方法始终在对象的thread()中执行。这使得它对于该对象是线程安全的,但当然在方法内部使用任何其他对象时也必须进行线程安全处理。

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

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

没有线程的对象需要特别小心处理。当一个对象的线程结束时,它就变成了没有线程的对象。然而,仅仅因为对象没有线程并不意味着它的所有方法都是线程安全的。最好选择一个线程来“拥有”这些对象,以实现线程安全。这个线程可能是主线程:

Q_ASSERT(QThread::currentThread() == (thread() ? thread() : qApp()->thread()));
我们的工作就是要实现这个断言。具体方法如下:
  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);
  }
};

这种方法能够维护断言,但过于冗长,并且每个参数都执行了额外的动态分配(至少在Qt 5.7版本中)。

  • 将调用分派到对象的线程中的仿函数。

    许多方法可以实现这一点;让我们介绍一种最小化动态分配数量的方法:在大多数情况下,仅需要一个。

    我们可以将该方法的调用包装在一个仿函数中,并确保它以线程安全的方式执行:

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

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

    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)));
    }
    
  • 将调用分派到对象的线程。

    这是以上内容的一种变体,但不使用函数对象(functor)。postCall函数可以显式地包装参数:

  • 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://dev59.com/B2sz5IYBdhLWcg3wfn-6#7858971
       // See also https://dev59.com/j2_Xa4cB1Zd3GeqP1nsb#15338881
       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表达您的意图,并通过构造函数避免未释放的互斥锁错误。

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

    使用特定于对象的互斥锁和在对象线程中调用方法体之间的选择取决于应用程序的需要。如果方法中访问的所有成员都是私有的,那么使用互斥锁是有意义的,因为我们通过设计可以确保所有访问都受到保护。使用特定于对象的互斥锁也将方法与对象事件循环上的争用解耦-因此可能具有性能优势。另一方面,如果方法必须访问不拥有的对象上的线程不安全的方法,则互斥锁将不够用,应在对象的线程中执行方法体。

    读取简单成员变量

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

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

    修改简单成员变量

    如果方法修改的是可以用QAtomicIntegerQAtomicPointer进行包装的单个数据,并且该操作可以使用原子原语完成,则可以使用原子字段:

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

    一般情况下,这种方法并不适用于修改多个成员变量:在某些成员变量被改变而其他成员变量未改变的中间状态将会对其他线程可见。通常情况下,这会破坏其他代码所依赖的不变量。


    请提供一些返回值(而不是void)的方法的示例。 - Nikita

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接