c++ qt thread-safety qthread

c++ - qt connect



¿Cómo aprovechar Qt para hacer que un método QObject sea seguro para subprocesos? (1)

Supongamos que escribimos un método no constante en una clase QObject -deriving:

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

Queremos que ese método sea seguro para subprocesos: lo que significa que llamarlo desde un subproceso arbitrario y desde varios subprocesos al mismo tiempo, no debería introducir un comportamiento indefinido.

  1. ¿Qué mecanismos / API proporciona Qt para ayudarnos a hacer que ese método sea seguro para subprocesos?

  2. ¿Qué mecanismos / API de Qt se podrían usar cuando el método hace las "otras cosas" también?

  3. ¿Hay alguna clasificación posible de las "otras cosas" que informarían qué mecanismos / API específicos de Qt usar?

Fuera del tema están los mecanismos proporcionados por el estándar C ++ en sí mismo, y formas genéricas / no específicas de Qt para garantizar la seguridad de los subprocesos.


Las API Qt aplicables dependen de cuál sea la funcionalidad del método seguro para subprocesos. Cubramos las circunstancias de lo más general a lo más específico.

Señales

Los cuerpos de señales son generados por la herramienta moc y son seguros para subprocesos.

Corolario 1: Todos los slots / functors conectados directamente deben ser seguros para subprocesos : de lo contrario, se rompe el contrato de una señal . Mientras que el sistema de ranura de señal permite el desacoplamiento de código, el caso específico de una conexión directa pierde los requisitos de una señal para el código conectado.

Corolario 2: las conexiones directas se acoplan más apretadas que las conexiones automáticas.

Haciendo el trabajo en el hilo del objeto

El enfoque más general es asegurar que el método siempre se ejecute en el thread() del objeto thread() . Esto lo hace seguro para subprocesos con respecto al objeto, pero, por supuesto, el uso de cualquier otro objeto dentro del método también debe realizarse de manera segura.

En general, un método inseguro de subproceso solo se puede llamar desde el thread() del objeto thread() :

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

El caso especial de un objeto sin hilos requiere un poco de cuidado. Un objeto queda sin hilos cuando termina su hilo. Sin embargo, el hecho de que el objeto no tenga subprocesos no hace que todos sus métodos sean seguros para subprocesos. Sería preferible elegir un subproceso para "poseer" dichos objetos con el propósito de seguridad de subprocesos. Tal hilo podría ser el hilo principal:

Q_ASSERT(QThread::currentThread() == (thread() ? thread() : qApp()->thread()));

Nuestro trabajo es cumplir esa afirmación. Así es cómo:

  1. Aproveche las señales a prueba de hilos.

    Como las señales son seguras para subprocesos, podríamos convertir nuestro método en una señal y alojar su implementación en una ranura:

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

    Este enfoque funciona para mantener la afirmación, pero es detallado y realiza una asignación dinámica adicional por cada argumento (a partir de Qt 5.7 al menos).

  2. Despacha la llamada en un functor al hilo del objeto.

    Hay muchas formas de hacerlo ; vamos a presentar uno que haga el número mínimo de asignaciones dinámicas: en la mayoría de los casos, exactamente uno.

    Podemos envolver la llamada del método en un functor y asegurarnos de que se ejecute de manera segura:

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

    No hay gastos generales ni copia de datos si el hilo actual es el hilo del objeto. De lo contrario, la llamada se diferirá al bucle de eventos en el hilo del objeto, o al bucle de eventos principal si el objeto no tiene hilos.

    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. Despacha la llamada al hilo del objeto.

    Esta es una variación de lo anterior, pero sin usar un functor. La función postCall puede ajustar los parámetros explícitamente:

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

    Entonces:

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

Protegiendo los datos del objeto

Si el método funciona en un conjunto de miembros, el acceso a estos miembros se puede serializar mediante el uso de un mutex. Aproveche QMutexLocker para expresar su intención y evitar errores mutex inéditos por construcción.

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

La elección entre usar un mutex específico de objeto e invocar el cuerpo del método en el hilo del objeto depende de las necesidades de la aplicación. Si todos los miembros a los que se accede en el método son privados, usar un mutex tiene sentido ya que tenemos el control y podemos asegurar, por diseño, que todo el acceso está protegido. El uso de mutex específico de objeto también desacopla el método de la contención en el bucle de eventos del objeto, por lo que podría tener beneficios de rendimiento. Por otro lado, si el método debe acceder a métodos inseguros para subprocesos en objetos que no posee, un mutex sería insuficiente y el cuerpo del método debería ejecutarse en el subproceso del objeto.

Lectura de una variable de miembro simple

Si el método const lee una sola pieza de datos que se puede incluir en un QAtomicInteger o QAtomicPointer , podemos usar un campo atómico:

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

Modificar una variable de miembro simple

Si el método modifica una sola pieza de datos que se puede envolver en QAtomicInteger o QAtomicPointer , y la operación se puede hacer usando una primitiva atómica, podemos usar un campo atómico:

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

Este enfoque no se extiende a la modificación de varios miembros en general: los estados intermedios donde algunos miembros se cambian y otros no, serán visibles para otros hilos. Por lo general, esto rompería los invariantes de los que depende otro código.