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

假设我们在一个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特定方法。

适用的Qt API取决于线程安全方法的function。 让我们从最一般的情况到最具体的情况。

信号

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

推论1: 所有直接连接的插槽/仿函数都必须是线程安全的 :否则会中断信号的合约 。 虽然信号插槽系统允许将代码解耦,但是直接连接的特定情况会将信号的要求泄露给连接的代码!

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

在对象的线程中做这项工作

最一般的方法是确保方法总是在对象的thread() 。 这使得它对于对象来说是线程安全的,但是当然在方法中使用任何其他对象也必须线程安全地进行。

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

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

无线对象的特殊情况需要注意。 线程完成时,对象变为无线程。 但是,仅仅因为对象是无线程的,并不能使它的所有方法都是线程安全的。 为了线程安全的目的,最好select一个线程来“拥有”这样的对象。 这样的线程可能是主线程:

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

    这种方法可以维护这个断言,但是是冗长的,并且对每个参数执行额外的dynamic分配(至less在Qt 5.7中)。

  2. 调用仿函数调用对象的线程。

    有很多方法可以做到这一点 。 让我们给出一个dynamic分配的最小数量:在大多数情况下,只有一个。

    我们可以将该方法的调用封装在函数中,并确保它是安全地执行的:

     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))); } 
  3. 调用对象线程的调用。

    这是上面的一个变种,但没有使用函子。 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://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来expression您的意图,并通过构build避免未发布的互斥错误。

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

在对象的线程中使用对象特定的互斥体和调用方法主体之间的select取决于应用程序的需求。 如果在方法中访问的所有成员都是私有的,那么使用互斥锁是有道理的,因为我们处于控制之下,并且可以通过devise确保所有访问都受到保护。 特定于对象的互斥体的使用也将方法与对象的事件循环中的争用分离 – 所以可能具有性能优点。 另一方面,该方法是否必须访问它不拥有的对象的线程不安全的方法,然后互斥体将不足,并且方法的主体应该在对象的线程中执行。

读一个简单的成员variables

如果const方法读取一个可以封装在QAtomicIntegerQAtomicPointer ,我们可以使用一个primefaces字段:

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

修改一个简单的成员variables

如果该方法修改了可以包装在QAtomicIntegerQAtomicPointer的单个数据, 并且可以使用primefaces基元完成操作,那么我们可以使用一个primefaces字段:

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

这种方法并没有扩展到通常修改多个成员:其他一些成员被更改而另一些成员不被更改的中间状态将被其他线程看到。 通常这会破坏其他代码所依赖的不variables。