Как выполнить функтор или лямбду в заданном потоке в стиле Qt, GCD?
В ObjC с GCD есть способ выполнить лямбду в любом из потоков, которые вращают цикл события. Например:
dispatch_sync(dispatch_get_main_queue(), ^{ /* do sth */ });
или же:
dispatch_async(dispatch_get_main_queue(), ^{ /* do sth */ });
Он выполняет что-то (эквивалентно []{ /* do sth */ }
в C++) в очереди основного потока, либо блокируя, либо асинхронно.
Как я могу сделать то же самое в 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);
}
4 ответа
Это конечно возможно. Любое решение будет основано на доставке события, которое оборачивает функтор в объект-потребитель, находящийся в нужном потоке. Мы назовем эту операцию мета-звонком. Подробности могут быть выполнены несколькими способами.
Qt 5.10 и выше TL;DR
// invoke on the main thread
QMetaObject::invokeMethod(qApp, []{ ... });
// invoke on an object's thread
QMetaObject::invokeMethod(obj, []{ ... });
// invoke on a particular thread
QMetaObject::invokeMethod(QAbstractEventDispatcher::instance(thread),
[]{ ... });
TL;DR для функторов
// https://github.com/KubaO/stackrun/tree/master/questions/metacall-21646467
// Qt 5.10 & up - it's 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's 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's 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
Функции публикации метаколла, в их самой простой форме, требуют, чтобы потребитель вызова функтора предоставил объект для данного потока и создал событие вызова функтора. Реализация мероприятия все еще впереди и является существенным отличием между различными реализациями.
Вторая перегрузка принимает ссылку на 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
сигнал и отправляет метаколл в прокси-объект.
Это, пожалуй, самая краткая и понятная реализация в стиле C++11. signalSource
Объект используется в C++11 RAII для побочных эффектов его разрушения. Фраза "побочные эффекты" имеет значение в семантике C++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 Destructor
Тот же подход может быть применен к 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 с использованием частного 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's 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
Есть один новый подход, который мне кажется самым простым. Это из Qt 5.4. Ссылка на документацию
void QTimer::singleShot(int msec, const QObject *context, Functor functor)
Пример:
QTimer::singleShot(0, qApp, []()
{
qDebug() << "hi from event loop";
});
Лямбда будет выполняться в потоке qApp (основной поток). Вы можете заменить контекст любым QObject по вашему желанию.
обновленный
QTimer нужен цикл обработки событий. Для потоков без цикла событий qt (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();
}
Может ли что-то подобное быть полезным?
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));
}
Этот кусок кода заставит вашу лямбду запускаться в цикле событий основного потока, как только это станет возможным. Нет поддержки аргументов, это очень простой код.
ПРИМЕЧАНИЕ: я не проверял это должным образом.
У других есть отличные ответы. вот мое предложение. вместо того, чтобы делать что-то вроде этого:-
QMetaObject::invokeMethod(socketManager,"newSocket",
Qt::QueuedConnection,
Q_ARG(QString, host),
Q_ARG(quint16, port.toUShort()),
Q_ARG(QString, username),
Q_ARG(QString, passhash)
);
сделайте что-нибудь вроде этого, что более приятно:-
QMetaObject::invokeMethod(socketManager,[=](){
socketManager->newSocket(host,port.toUShort(),username,passhash);
},Qt::QueuedConnection);
Я понятия не имею, о чем вы говорите, но я постараюсь ответить на это любым способом.
Допустим, у вас есть класс с функцией слота
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 в соединение, то оно будет помещать вызов слота в цикл обработки событий.
Вы также можете легко обработать сигнал, но это будет автоматически в очереди.
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 вы можете даже создать слот в линии внутри соединения. Но как только вы используете лямбды, я понятия не имею, как с ними работают потоки. Насколько я знаю, вам нужен объект, который должен находиться в потоке, чтобы вызвать вызов слота из основного потока.