Подтвердить что ты не робот

Как выполнить функтор или лямбда в заданном потоке в Qt, GCD-стиле?

В ObjC с GCD существует способ выполнения лямбда в любом из потоков, которые вращают цикл событий. Например:

dispatch_sync(dispatch_get_main_queue(), ^{ /* do sth */ });

или

dispatch_async(dispatch_get_main_queue(), ^{ /* do sth */ });

Он выполняет что-то (эквивалентное []{ /* do sth */ } в С++) в очереди основного потока, либо блокируя, либо асинхронно.

Как я могу сделать то же самое в Qt?

Из того, что я прочитал, я думаю, что решение было бы каким-то образом отправить сигнал на какой-то объект основного потока. Но какой объект? Просто QApplication::instance()? (Это единственный объект, живущий в основном потоке в этой точке.) И какой сигнал?


Из текущих ответов и моих текущих исследований действительно кажется, что мне нужен какой-то фиктивный объект для сидения в основном потоке с некоторым слотом, который просто ждет, чтобы попасть в какой-то код для выполнения.

Итак, я решил подклассом QApplication добавить это. Мой текущий код, который не работает (но, возможно, вы можете помочь):

#include <QApplication>
#include <QThread>
#include <QMetaMethod>
#include <functional>
#include <assert.h>

class App : public QApplication
{
    Q_OBJECT

public:
    App();

signals:

public slots:
    void genericExec(std::function<void(void)> func) {
        func();
    }

private:
    // cache this
    QMetaMethod genericExec_method;
public:
    void invokeGenericExec(std::function<void(void)> func, Qt::ConnectionType connType) {
        if(!genericExec_method) {
            QByteArray normalizedSignature = QMetaObject::normalizedSignature("genericExec(std::function<void(void)>)");
            int methodIndex = this->metaObject()->indexOfSlot(normalizedSignature);
            assert(methodIndex >= 0);
            genericExec_method = this->metaObject()->method(methodIndex);
        }
        genericExec_method.invoke(this, connType, Q_ARG(std::function<void(void)>, func));
    }

};

static inline
void execInMainThread_sync(std::function<void(void)> func) {
    if(qApp->thread() == QThread::currentThread())
        func();
    else {
        ((App*) qApp)->invokeGenericExec(func, Qt::BlockingQueuedConnection);
    }
}

static inline
void execInMainThread_async(std::function<void(void)> func) {
    ((App*) qApp)->invokeGenericExec(func, Qt::QueuedConnection);
}
4b9b3361

Ответ 1

Это, безусловно, возможно. Любое решение сосредоточится на доставке события, которое обертывает функтор на объект-потребитель, проживающий в нужном потоке. Мы будем называть эту операцию метакалированием. Детали могут быть выполнены несколькими способами.

Qt 5,10 и выше TL; DR

// invoke on the main thread
QMetaObject::invokeMethod(qApp, []{ ... });

// invoke on an object thread
QMetaObject::invokeMethod(obj, []{ ... });

// invoke on a particular thread
QMetaObject::invokeMethod(QAbstractEventDispatcher::instance(thread),
                         []{ ... });

TL; DR для функторов

// https://github.com/KubaO/stackoverflown/tree/master/questions/metacall-21646467

// Qt 5.10 & up - it all done

template <typename F>
static void postToObject(F &&fun, QObject *obj = qApp) {
  QMetaObject::invokeMethod(obj, std::forward<F>(fun));
}

template <typename F>
static void postToThread(F && fun, QThread *thread = qApp->thread()) {
   auto *obj = QAbstractEventDispatcher::instance(thread);
   Q_ASSERT(obj);
   QMetaObject::invokeMethod(obj, std::forward<F>(fun));
}

// Qt 5/4 - preferred, has least allocations

namespace detail {
template <typename F>
struct FEvent : public QEvent {
   using Fun = typename std::decay<F>::type;
   Fun fun;
   FEvent(Fun && fun) : QEvent(QEvent::None), fun(std::move(fun)) {}
   FEvent(const Fun & fun) : QEvent(QEvent::None), fun(fun) {}
   ~FEvent() { fun(); }
}; }

template <typename F>
static void postToObject(F && fun, QObject * obj = qApp) {
   if (qobject_cast<QThread*>(obj))
      qWarning() << "posting a call to a thread object - consider using postToThread";
   QCoreApplication::postEvent(obj, new detail::FEvent<F>(std::forward<F>(fun)));
}

template <typename F>
static void postToThread(F && fun, QThread * thread = qApp->thread()) {
   QObject * obj = QAbstractEventDispatcher::instance(thread);
   Q_ASSERT(obj);
   QCoreApplication::postEvent(obj, new detail::FEvent<F>(std::forward<F>(fun)));
}
// Qt 5 - alternative version

template <typename F>
static void postToObject2(F && fun, QObject * obj = qApp) {
   if (qobject_cast<QThread*>(obj))
      qWarning() << "posting a call to a thread object - consider using postToThread";
   QObject src;
   QObject::connect(&src, &QObject::destroyed, obj, std::forward<F>(fun),
                    Qt::QueuedConnection);
}

template <typename F>
static void postToThread2(F && fun, QThread * thread = qApp->thread()) {
   QObject * obj = QAbstractEventDispatcher::instance(thread);
   Q_ASSERT(obj);
   QObject src;
   QObject::connect(&src, &QObject::destroyed, obj, std::forward<F>(fun),
                    Qt::QueuedConnection);
}
void test1() {
   QThread t;
   QObject o;
   o.moveToThread(&t);

   // Execute in given object thread
   postToObject([&]{ o.setObjectName("hello"); }, &o);
   // or
   postToObject(std::bind(&QObject::setObjectName, &o, "hello"), &o);

   // Execute in given thread
   postToThread([]{ qDebug() << "hello from worker thread"; });

   // Execute in the main thread
   postToThread([]{ qDebug() << "hello from main thread"; });
}

TL; DR для методов/слотов

// Qt 5/4
template <typename T, typename R>
static void postToObject(T * obj, R(T::* method)()) {
   struct Event : public QEvent {
      T * obj;
      R(T::* method)();
      Event(T * obj, R(T::*method)()):
         QEvent(QEvent::None), obj(obj), method(method) {}
      ~Event() { (obj->*method)(); }
   };
   if (qobject_cast<QThread*>(obj))
      qWarning() << "posting a call to a thread object - this may be a bug";
   QCoreApplication::postEvent(obj, new Event(obj, method));
}

void test2() {
   QThread t;
   struct MyObject : QObject { void method() {} } obj;
   obj.moveToThread(&t);

   // Execute in obj thread
   postToObject(&obj, &MyObject::method);
}

TL; DR: Как насчет таймера одиночного выстрела?

Все описанные выше методы работают из потоков, не имеющих цикла событий. Из-за QTBUG-66458 удобное присвоение QTimer::singleShot нуждается в цикле событий в исходном потоке. Затем postToObject становится очень простым, и вы можете просто использовать QTimer::singleShot напрямую, хотя это неловкое имя, которое скрывает намерение от тех, кто не знаком с этой идиомой. Косвенность с помощью функции, названной для лучшего указания, имеет смысл, даже если вам не нужна проверка типа:

template <typename F>
static void postToObject(F && fun, QObject * obj = qApp) {
   if (qobject_cast<QThread*>(obj))
      qWarning() << "posting a call to a thread object - consider using postToThread";
   QTimer::singleShot(0, obj, std::forward<F>(fun));
}

Общий код

Давайте определим нашу проблему с точки зрения следующего общего кода. Простейшие решения отправят событие либо в объект приложения, если целевой поток является основным потоком, либо диспетчер событий для любого другого заданного потока. Поскольку диспетчер событий будет существовать только после QThread::run, мы указываем требование для запуска потока, возвращая true из needsRunningThread.

#ifndef HAS_FUNCTORCALLCONSUMER
namespace FunctorCallConsumer {
   bool needsRunningThread() { return true; }
   QObject * forThread(QThread * thread) {
      Q_ASSERT(thread);
      QObject * target = thread == qApp->thread()
            ? static_cast<QObject*>(qApp) : QAbstractEventDispatcher::instance(thread);
      Q_ASSERT_X(target, "postMetaCall", "the receiver thread must have an event loop");
      return target;
   }
}
#endif

Функции отправки метаданных в их простейшей форме требуют, чтобы потребитель-вызов functor предоставлял объект для данного потока и создавал экземпляр события вызова functor. Реализация мероприятия все еще впереди нас, и это существенное различие между различными реализациями.

Вторая перегрузка принимает ссылку rvalue для функтора, потенциально сохраняя операцию копирования на функторе. Это полезно, если продолжение содержит данные, которые дорого копировать.

#ifndef HAS_POSTMETACALL
void postMetaCall(QThread * thread, const std::function<void()> & fun) {
   auto receiver = FunctorCallConsumer::forThread(thread);
   QCoreApplication::postEvent(receiver, new FunctorCallEvent(fun, receiver));
}

void postMetaCall(QThread * thread, std::function<void()> && fun) {
   auto receiver = FunctorCallConsumer::forThread(thread);
   QCoreApplication::postEvent(receiver,
                               new FunctorCallEvent(std::move(fun), receiver));
}
#endif

Для демонстрационных целей рабочий поток сначала помещает метакальку в основной поток, а затем отсылает QThread::run() чтобы запустить цикл событий, чтобы прослушивать возможные метакали с других потоков. Мьютекс используется для того, чтобы пользователь потока мог подождать простым способом для запуска потока, если это необходимо для реализации потребителя. Такое ожидание необходимо для указанного пользователем события по умолчанию.

class Worker : public QThread {
   QMutex m_started;
   void run() {
      m_started.unlock();
      postMetaCall(qApp->thread(), []{
         qDebug() << "worker functor executes in thread" << QThread::currentThread();
      });
      QThread::run();
   }
public:
   Worker(QObject * parent = 0) : QThread(parent) { m_started.lock(); }
   ~Worker() { quit(); wait(); }
   void waitForStart() { m_started.lock(); m_started.unlock(); }
};

Наконец, мы запускаем вышеупомянутый рабочий поток, который помещает метакальку в основной (прикладной) поток, а поток приложения помещает метакальку в рабочий поток.

int main(int argc, char *argv[])
{
   QCoreApplication a(argc, argv);
   a.thread()->setObjectName("main");
   Worker worker;
   worker.setObjectName("worker");
   qDebug() << "worker thread:" << &worker;
   qDebug() << "main thread:" << QThread::currentThread();
   if (FunctorCallConsumer::needsRunningThread()) {
      worker.start();
      worker.waitForStart();
   }
   postMetaCall(&worker, []{ qDebug() << "main functor executes in thread" << QThread::currentThread(); });
   if (!FunctorCallConsumer::needsRunningThread()) worker.start();
   QMetaObject::invokeMethod(&a, "quit", Qt::QueuedConnection);
   return a.exec();
}

Результат будет выглядеть примерно следующим образом во всех реализациях. Функторы пересекают потоки: созданный в основном потоке выполняется в рабочем потоке и наоборот.

worker thread: QThread(0x7fff5692fc20, name = "worker") 
main thread: QThread(0x7f86abc02f00, name = "main") 
main functor executes in thread QThread(0x7fff5692fc20, name = "worker") 
worker functor executes in thread QThread(0x7f86abc02f00, name = "main") 

Решение Qt 5 с использованием временного объекта в качестве источника сигнала

Самый простой подход для Qt 5 заключается в использовании временного QObject в качестве источника сигнала и подключении функтора к его destroyed(QObject*) сигналу. Когда postMetaCall возвращается, signalSource разрушается, испускает свой destroyed сигнал и помещает метакальку в прокси-объект.

Это, пожалуй, самая краткая и простая реализация в стиле С++ 11. Объект signalSource используется в signalSource С++ 11 RAII для побочных эффектов его разрушения. Фраза "побочные эффекты" имеет смысл в семантике С++ 11 и не должна интерпретироваться как "ненадежная" или "нежелательная" - это ничего, кроме. Контракт QObject с нами заключается в том, чтобы destroyed когда-то во время выполнения его деструктора. Мы более чем рады использовать этот факт.

#include <QtCore>
#include <functional>

namespace FunctorCallConsumer { QObject * forThread(QThread*); }

#define HAS_POSTMETACALL
void postMetaCall(QThread * thread, const std::function<void()> & fun) {
   QObject signalSource;
   QObject::connect(&signalSource, &QObject::destroyed,
                    FunctorCallConsumer::forThread(thread), [=](QObject*){ fun(); });
}
#ifdef __cpp_init_captures
void postMetaCall(QThread * thread, std::function<void()> && fun) {
   QObject signalSource;
   QObject::connect(&signalSource, &QObject::destroyed,
                    FunctorCallConsumer::forThread(thread), [fun(std::move(fun))](QObject*){ fun(); });
}
#endif
// Common Code follows here

Если мы намерены публиковать только основной поток, код становится почти тривиальным:

void postToMainThread(const std::function<void()> & fun) {
  QObject signalSource;
  QObject::connect(&signalSource, &QObject::destroyed, qApp, [=](QObject*){
    fun();
  });
}

#ifdef __cpp_init_captures
void postToMainThread(std::function<void()> && fun) {
  QObject signalSource;
  QObject::connect(&signalSource, &QObject::destroyed, qApp, [fun(std::move(fun))](QObject*){
    fun();
  });
}
#endif

Решение Qt 4/5 с использованием деструктора QEvent

Тот же подход может применяться к QEvent напрямую. Виртуальный деструктор события может вызвать функтор. События удаляются сразу после их доставки диспетчером событий потока объектов-потребителей, поэтому они всегда выполняются в нужном потоке. Это не изменится в Qt 4/5.

#include <QtCore>
#include <functional>

class FunctorCallEvent : public QEvent {
   std::function<void()> m_fun;
   QThread * m_thread;
public:
   FunctorCallEvent(const std::function<void()> & fun, QObject * receiver) :
      QEvent(QEvent::None), m_fun(fun), m_thread(receiver->thread()) {}
   FunctorCallEvent(std::function<void()> && fun, QObject * receiver) :
      QEvent(QEvent::None), m_fun(std::move(fun)), m_thread(receiver->thread()) { qDebug() << "move semantics"; }
   ~FunctorCallEvent() {
      if (QThread::currentThread() == m_thread)
         m_fun();
      else
         qWarning() << "Dropping a functor call destined for thread" << m_thread;
   }
};
// Common Code follows here

Для публикации только в основной теме все становится еще проще:

class FunctorCallEvent : public QEvent {
   std::function<void()> m_fun;
public:
   FunctorCallEvent(const std::function<void()> & fun) :
      QEvent(QEvent::None), m_fun(fun) {}
   FunctorCallEvent(std::function<void()> && fun, QObject * receiver) :
      QEvent(QEvent::None), m_fun(std::move(fun)) {}
   ~FunctorCallEvent() {
      m_fun();
   }
};

void postToMainThread(const std::function<void()> & fun) {
   QCoreApplication::postEvent(qApp, new FunctorCallEvent(fun);
}

void postToMainThread(std::function<void()> && fun) {
   QCoreApplication::postEvent(qApp, new FunctorCallEvent(std::move(fun)));
}

Решение Qt 5 с использованием Private QMetaCallEvent

Функтор можно обернуть в полезную нагрузку объекта Qt 5 слота QMetaCallEvent. Функтор будет вызван QObject::event и, следовательно, может быть отправлен на любой объект в целевом потоке. Это решение использует частные детали реализации Qt 5.

#include <QtCore>
#include <private/qobject_p.h>
#include <functional>

class FunctorCallEvent : public QMetaCallEvent {
public:
   template <typename Functor>
   FunctorCallEvent(Functor && fun, QObject * receiver) :
      QMetaCallEvent(new QtPrivate::QFunctorSlotObject<Functor, 0, typename QtPrivate::List_Left<void, 0>::Value, void>
                     (std::forward<Functor>(fun)), receiver, 0, 0, 0, (void**)malloc(sizeof(void*))) {}
   // Metacalls with slot objects require an argument array for the return type, even if it void.
};
// Common Code follows here

Решение Qt 4/5 с использованием пользовательского события и потребителя

Мы переопределяем метод event() объекта и называем его функтором. Это вызывает явный объект-объект события в каждом потоке, на который отправляются функторы. Объект очищается, когда его поток завершен, или для основного потока, когда экземпляр приложения разрушен. Он работает как на Qt 4, так и на Qt 5. Использование ссылок rvalue позволяет избежать копирования временного функтора.

#include <QtCore>
#include <functional>

class FunctorCallEvent : public QEvent {
   std::function<void()> m_fun;
public:
   FunctorCallEvent(const std::function<void()> & fun, QObject *) :
      QEvent(QEvent::None), m_fun(fun) {}
   FunctorCallEvent(std::function<void()> && fun, QObject *) :
      QEvent(QEvent::None), m_fun(std::move(fun)) { qDebug() << "move semantics"; }
   void call() { m_fun(); }
};

#define HAS_FUNCTORCALLCONSUMER
class FunctorCallConsumer : public QObject {
   typedef QMap<QThread*, FunctorCallConsumer*> Map;
   static QObject * m_appThreadObject;
   static QMutex m_threadObjectMutex;
   static Map m_threadObjects;
   bool event(QEvent * ev) {
      if (!dynamic_cast<FunctorCallEvent*>(ev)) return QObject::event(ev);
      static_cast<FunctorCallEvent*>(ev)->call();
      return true;
   }
   FunctorCallConsumer() {}
   ~FunctorCallConsumer() {
      qDebug() << "consumer done for thread" << thread();
      Q_ASSERT(thread());
      QMutexLocker lock(&m_threadObjectMutex);
      m_threadObjects.remove(thread());
   }
   static void deleteAppThreadObject() {
      delete m_appThreadObject;
      m_appThreadObject = nullptr;
   }
public:
   static bool needsRunningThread() { return false; }
   static FunctorCallConsumer * forThread(QThread * thread) {
      QMutexLocker lock(&m_threadObjectMutex);
      Map map = m_threadObjects;
      lock.unlock();
      Map::const_iterator it = map.find(thread);
      if (it != map.end()) return *it;
      FunctorCallConsumer * consumer = new FunctorCallConsumer;
      consumer->moveToThread(thread);
      if (thread != qApp->thread())
         QObject::connect(thread, SIGNAL(finished()), consumer, SLOT(deleteLater()));
      lock.relock();
      it = m_threadObjects.find(thread);
      if (it == m_threadObjects.end()) {
         if (thread == qApp->thread()) {
            Q_ASSERT(! m_appThreadObject);
            m_appThreadObject = consumer;
            qAddPostRoutine(&deleteAppThreadObject);
         }
         m_threadObjects.insert(thread, consumer);
         return consumer;
      } else {
         delete consumer;
         return *it;
      }
   }
};

QObject * FunctorCallConsumer::m_appThreadObject = nullptr;
QMutex FunctorCallConsumer::m_threadObjectMutex;
FunctorCallConsumer::Map FunctorCallConsumer::m_threadObjects;
// Common Code follows here

Ответ 2

Может ли что-то подобное быть полезным?

template <typename Func>
inline static void MyRunLater(Func func) {
    QTimer *t = new QTimer();
    t->moveToThread(qApp->thread());
    t->setSingleShot(true);
    QObject::connect(t, &QTimer::timeout, [=]() {
        func();
        t->deleteLater();
    });
    QMetaObject::invokeMethod(t, "start", Qt::QueuedConnection, Q_ARG(int, 0));
}

Этот фрагмент кода сделает ваш лямбда-побег в цикле событий основного потока, как только это станет возможным. Нет поддержки args, это очень простой код.

ПРИМЕЧАНИЕ. Я не тестировал его должным образом.

Ответ 3

Есть один новый подход, который я считаю самым легким. Это от Qt 5.4. Ссылка на документацию

void QTimer::singleShot(int msec, const QObject *context, Functor functor)

Пример:

QTimer::singleShot(0, qApp, []()
{
    qDebug() << "hi from event loop";
});

lambda будет выполняться в потоке qApp (основной поток). Вы можете заменить контекст любым QObject, который вы хотите.

Обновление

QTimer нуждается в цикле событий для работы. Для потоков без цикла qt event (std:: thread) мы могли бы создать его. Код для запуска лямбда в std:: thread.

QEventLoop loop;
Q_UNUSED(loop)
QTimer::singleShot(0, qApp, []()
{
    qDebug() << "singleShot from std thread";
});

Полный пример

#include <QCoreApplication>
#include <QTimer>
#include <QDebug>
#include <thread>
#include <QThread>
#include <QEventLoop>
#include <QThread>
using std::thread;

class TestObj
        :public QObject
{
// Used new connect syntax no need for Q_OBJECT define
// you SHOULD use it. I used just to upload one file
//Q_OBJECT
public slots:
    void doWork()
    {
        qDebug() << "QThread id" << QThread::currentThreadId();
        QTimer::singleShot(0, qApp, []()
        {
            qDebug() << "singleShot from QThread" << QThread::currentThreadId();
        });
    }
};


int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    qDebug() << "main thread id" << QThread::currentThreadId();

    thread testThread([]()
    {
        QEventLoop loop;
        Q_UNUSED(loop)
        qDebug() << "std::thread id" << QThread::currentThreadId();

        QTimer::singleShot(0, qApp, []()
        {
            qDebug() << "singleShot from std thread" << QThread::currentThreadId();
        });
        qDebug() << "std::thread finished";
    });
    testThread.detach();

    QThread testQThread;
    TestObj testObj;
    testObj.moveToThread(&testQThread);
    QObject::connect(&testQThread, &QThread::started, &testObj, &TestObj::doWork);
    testQThread.start();

    return a.exec();
}

Ответ 4

Я понятия не имею, о чем вы говорите, но я попытаюсь ответить на него любым способом.

Предположим, что у вас есть класс с функцией focntion слота

class MyClass : public QObject
{
    Q_OBJECT
public:
    MyClass() {}

public slots: 
    void MySlot() { qDebug() << "RAWR";
};

Нет, если вы хотите запустить этот синхронно в основном потоке, вы можете напрямую вызвать эту функцию. Чтобы подключить сигнал, вам необходимо создать объект и подключить сигнал к слоту.

class MySignalClass : public QObject
{
    Q_OBJECT
public:
    MySignalClass() {}

    signalSomthign() { emit someAwesomeSignal; }

public signals: 
    void someAwesomeSignal();
};

И где-то в главном потоке вы делаете что-то вроде

MyClass slotClass;
MySignalClass signalClass;
qobject::connect(&signalClass, SIGNAL(someAwesomeSignal), &slotClass(), SLOT(MySlot)));

Итак, теперь, если вы можете подключить несколько сигналов к этому объекту слота, но реалистично предоставленный мной код не будет работать иначе, чем обычный вызов функции. Вы сможете увидеть это с помощью трассировки стека. Если вы добавите флаг qobject:: queuedConneciton к подключению, он перевернет вызов слота в цикле событий.

Вы также можете легко прорисовать сигнал, но это автоматически будет queuedConnection

MyClass slotClass;
MySignalClass signalClass;
QThread someThread;
slotClass.moveToThread(&someThread);
qobject::connect(&signalClass, SIGNAL(someAwesomeSignal), &slotClass(), SLOT(MySlot)));

Теперь вы будете иметь потоковый сигнал в основном. Если ваш сигнал будет потоковым, тогда вам нужно только переключиться на signalClass.moveToThread(& someThread), когда выдается сигнал. SignalClass будет запускаться в основном потоке.

Если вы не хотите, чтобы вызываемый объект вызывался, я не уверен, что lamdas может работать. Я использовал их раньше, но я думаю, что они все равно должны быть завернуты в класс.

qobject::connect(&signalClass, &slotClass::MySlot, [=]() { /* whatever */ });

Хотя я уверен, что с Qt5 вы даже можете зайти так далеко, как создать слот в строке внутри соединения. Но как только вы используете lambdas, я не знаю, как работают с ними потоки. Насколько я знаю, вам нужен объект, чтобы сидеть в потоке, в основном для принудительного вызова слота из основного потока.