Что происходит с близостью потока объекта QObject, созданного в рабочем потоке, который затем завершается?
Скажем, я вызываю QtConcurrent::run()
, который запускает функцию в рабочем потоке, и в этой функции я динамически выделяю несколько QObjects (для последующего использования). Поскольку они были созданы в рабочем потоке, их сродство к потоку должно быть связано с рабочим потоком. Однако, как только рабочий поток завершается, сродство потока QObject больше не будет действительным.
Вопрос: автоматически ли Qt переносит QObjects в родительский поток или не несет ответственности за перенос их в допустимый поток до завершения рабочего цикла?
Ответы
Ответ 1
QThread
не документируется, чтобы автоматически перемещать любой QObject
, когда он заканчивается, поэтому я думаю, что мы уже можем сделать вывод, что он не делает этого. Такое поведение было бы очень неожиданным и противоречило бы остальным API.
Просто для полноты, я тестировал с Qt 5.6:
QObject o;
{
QThread t;
o.moveToThread(&t);
for (int i = 0; i < 2; ++i)
{
t.start();
QVERIFY(t.isRunning());
QVERIFY(o.thread() == &t);
t.quit();
t.wait();
QVERIFY(t.isFinished());
QVERIFY(o.thread() == &t);
}
}
QVERIFY(o.thread() == nullptr);
Напомним, что a QThread
не является потоком, он управляет потоком.
Когда заканчивается QThread
, он продолжает существовать, и объекты, которые в нем живут, продолжают жить в нем, но они больше не обрабатывают события. QThread
можно перезапустить (не рекомендуется), после чего обработка событий будет возобновлена (таким образом, тот же QThread
мог бы управлять другим потоком).
Когда a QThread
уничтожается, объекты, которые в нем живут, перестают иметь сходство нитей. Документация не гарантирует этого, и на самом деле говорит: "Вы должны убедиться, что все объекты, созданные в потоке, удалены, прежде чем удалить QThread
".
Скажем, я вызываю QtConcurrent::run()
, который запускает функцию в рабочем потоке, и в этой функции я динамически выделяю несколько QObjects (для последующего использования). Поскольку они были созданы в рабочем потоке, их сродство к потоку должно быть связано с рабочим потоком. Однако, как только рабочий поток завершается, сродство потока QObject больше не будет действительным.
QThread
не заканчивается в этом сценарии. Когда завершается завершение задания QtConcurrent::run
, QThread
, который он запускал, возвращается в QThreadPool
и может быть повторно использован последующим вызовом QtConcurrent::run
, а QObject
, живущим в этом QThread
, продолжит жить там.
QThreadPool::globalInstance()->setMaxThreadCount(1);
QObject *o = nullptr;
QThread *t = nullptr;
QFuture<void> f = QtConcurrent::run([&] {
o = new QObject;
t = o->thread();
QVERIFY(t == QThread::currentThread());
});
f.waitForFinished();
QVERIFY(t == o->thread());
QVERIFY(t->isRunning());
f = QtConcurrent::run([=] {
QVERIFY(t == QThread::currentThread());
});
f.waitForFinished();
Вы можете вручную перенести объект из QThread
, прежде чем он будет возвращен в QThreadPool
, или просто не используйте QtConcurrent::run
. Имея конструкцию задачи QtConcurrent::run
QObject
, которая переживает задачу, является сомнительной конструкцией, задачи должны быть автономными. Как отметил @Mike, QThread
, используемый QtConcurrent::run
, не имеет циклов событий.
Ответ 2
Однако, как только рабочий поток завершается, аффинность потока QObject больше не будет действительна.
Рабочий поток завершает работу НЕ после вызова функции. Весь смысл использования QtConcurrent::run
выполняет большое количество небольших задач в глобальном пуле потоков (или в некоторых из них QThreadPool
), а повторно использовать потоки, чтобы избежать накладных расходов на создание и уничтожение потоков для каждой из этих небольших задач. В дополнение к распределению вычислений по всем доступным ядрам.
Вы можете попробовать посмотреть исходный код для Qt, чтобы увидеть как QtConcurrent::run
реализовано. Вы увидите, что он заканчивает вызов RunFunctionTaskBase::start
, который по существу вызывает QThreadPool::start
с QRunnable
, который вызывает функцию, которая была первоначально отправлена на QtConcurrent::run
.
Теперь я хочу, чтобы QThreadPool::start
был добавлен QRunnable
в очередь, и затем пытается пробудить один из потоков из пула потоков (которые ждут для добавления нового QRunnable
в очередь). Дело в том, что потоки из пула потоков не запускают цикл событий (они не предназначены для этого), они просто выполняют QRunnable
в очереди и ничего больше (они реализованы по соображениям эффективности).
Это означает, что, когда вы создаете QObject
в функции, выполняемой в QtConcurrent::run
, вы просто создаете QObject
, который живет в потоке без цикла событий, из docs, ограничения включают в себя:
Если цикл событий не запущен, события не будут доставлены объекту. Например, если вы создаете объект QTimer
в потоке, но никогда не вызываете exec()
, QTimer
никогда не будет излучать его сигнал timeout()
. Вызов deleteLater()
тоже не будет работать. (Эти ограничения распространяются и на основной поток.)
TL; DR: QtConcurrent::run
запускает функции в потоках из глобального QThreadPool
(или предоставленного). Эти потоки не запускают цикл событий, они просто ждут выполнения QRunnable
. Таким образом, QObject
, живущий в потоке из этих потоков, не получает никаких событий.
В документации, они поместили с помощью QThread
(возможно, с циклом события и рабочим объектом) и используя QtConcurrent::run
в качестве двух отдельных многопоточных технологий. Они не должны смешиваться. Таким образом, нет рабочих объектов в пулах потоков, это просто вызывает проблемы.
Вопрос: автоматически ли Qt переносит QObjects в родительский поток или не несет ответственности за перенос их в допустимый поток до завершения рабочего цикла?
Я думаю, что, посмотрев на вещи таким образом, ответ очевиден, что Qt делает НЕ перемещение QObject
в любой поток автоматически. Документация предупреждает об использовании QObject
в QThread
без цикла события и что он.
Вы можете перемещать их в любой поток, который вам нравится. Но имейте в виду, что moveToThread()
может иногда вызывать проблемы. Например, если перемещение вашего рабочего объекта связано с перемещением QTimer
:
Обратите внимание, что все активные таймеры для объекта будут reset. Таймеры сначала останавливаются в текущем потоке и перезапускаются (с тем же интервалом) в targetThread. В результате постоянное перемещение объекта между потоками может откладывать события таймера неограниченно.
Заключение: Я думаю, что вам стоит подумать о том, чтобы использовать свой собственный QThread
, который запускает цикл событий, и создайте вместо этого своего рабочего QObject
вместо QtConcurrent
. Этот способ намного лучше, чем перемещение QObject
вокруг, и может избежать многих ошибок, которые могут возникнуть в результате использования вашего текущего подхода. Посмотрите сравнительную таблицу технологий многопоточности в Qt и выберите технологию, которая наилучшим образом соответствует вашему варианту использования. Используйте только QtConcurrent
, если вы хотите просто выполнить функцию одного вызова и получить ее возвращаемое значение. Если вам нужно постоянное взаимодействие с потоком, вы должны переключиться на использование своего QThread
с рабочим QObject
s.
Ответ 3
Qt автоматически перемещает QObjects в родительский поток или не несет ответственности за перенос их в допустимый поток до завершения рабочего цикла?
Нет, Qt не автоматически перемещает QObject
в родительский поток.
Это поведение явно не задокументировано, поэтому я провел небольшое исследование структуры Qt исходного кода, главной ветки.
QThread
начинается с QThreadPrivate::start
:
unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(void *arg)
{
...
thr->run();
finish(arg);
return 0;
}
QThread::terminate()
реализация:
void QThread::terminate()
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
if (!d->running)
return;
if (!d->terminationEnabled) {
d->terminatePending = true;
return;
}
TerminateThread(d->handle, 0);
d->terminated = true;
QThreadPrivate::finish(this, false);
}
В обоих случаях завершение финализации выполняется в QThreadPrivate::finish
:
void QThreadPrivate::finish(void *arg, bool lockAnyway)
{
QThread *thr = reinterpret_cast<QThread *>(arg);
QThreadPrivate *d = thr->d_func();
QMutexLocker locker(lockAnyway ? &d->mutex : 0);
d->isInFinish = true;
d->priority = QThread::InheritPriority;
bool terminated = d->terminated;
void **tls_data = reinterpret_cast<void **>(&d->data->tls);
locker.unlock();
if (terminated)
emit thr->terminated();
emit thr->finished();
QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete);
QThreadStorageData::finish(tls_data);
locker.relock();
d->terminated = false;
QAbstractEventDispatcher *eventDispatcher = d->data->eventDispatcher;
if (eventDispatcher) {
d->data->eventDispatcher = 0;
locker.unlock();
eventDispatcher->closingDown();
delete eventDispatcher;
locker.relock();
}
d->running = false;
d->finished = true;
d->isInFinish = false;
if (!d->waiters) {
CloseHandle(d->handle);
d->handle = 0;
}
d->id = 0;
}
Он отправляет QEvent::DeferredDelete
событие для очистки QObject::deleteLater
, чем данные TLS, очищенные с помощью QThreadStorageData::finish(tls_data)
и eventDispatcher
удалены. После этого QObject
не получит никаких событий из этого потока, но QObject
сродство нитей остается неизменным. Интересно увидеть реализацию void QObject::moveToThread(QThread *targetThread)
, чтобы понять, как меняется аффинность потока.
Реализация void QThreadPrivate::finish(void *arg, bool lockAnyway)
дает понять, что QObject
сродство нитей не изменяется на QThread
.
Ответ 4
Хотя это старый вопрос, я недавно задал тот же вопрос и просто ответил на него с помощью QT 4.8 и некоторых тестов.
AFAIK вы не можете создавать объекты с родителем из функции QtConcurrent:: run. Я пробовал следующие два пути. Позвольте мне определить блок кода, тогда мы рассмотрим поведение, выбрав POINTER_TO_THREAD.
Какой-нибудь код psuedo покажет вам мой тест
Class MyClass : public QObject
{
Q_OBJECT
public:
doWork(void)
{
QObject* myObj = new QObject(POINTER_TO_THREAD);
....
}
}
void someEventHandler()
{
MyClass* anInstance = new MyClass(this);
QtConcurrent::run(&anInstance, &MyClass::doWork)
}
Игнорирование проблем с потенциальными проблемами...
Если для параметра POINTER_TO_THREAD
установлено значение this
, вы получите сообщение об ошибке, потому что this
разрешит указатель на объект anInstance
, который живет в основном потоке, а не поток, отправленный для него QtConcurrent, Вы увидите что-то вроде...
Cannot create children for a parent in another thread. Parent: anInstance, parents thread: QThread(xyz), currentThread(abc)
Если для параметра POINTER_TO_THREAD
установлено значение QObject::thread()
, вы получите сообщение об ошибке, потому что оно решит объект QThread, в котором живет anInstance
, а не поток, который QtConcurrent отправил для него. Вы увидите что-то вроде...
Cannot create children for a parent in another thread. Parent: QThread(xyz), parents thread: QThread(xyz), currentThread(abc)
Надеюсь, что мое тестирование будет полезным для кого-то другого. Если кто-то знает способ получить указатель на QThread, в котором QtConcurrent запускает метод, мне было бы интересно его услышать!
Ответ 5
Я не уверен, что Qt автоматически изменит аффинность потока. Но даже если это так, единственной разумной нитью для перехода является основной поток. Я бы сам нажал их в конце резьбовой функции.
myObject->moveToThread(QApplication::instance()->thread());
Теперь это имеет значение только в том случае, если объекты используют процесс обработки событий, например сигналы отправки и получения.
Ответ 6
Хотя в документах Qt не указано поведение, которое вы могли бы обнаружить, отслеживая, что QObject::thread()
возвращает до и после завершения потока.