Ответ 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