Что происходит с привязкой к потоку QObject, созданного в рабочем потоке, который затем завершается?

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

Вопрос: Qt автоматически перемещает объекты QObject в родительский поток или мы несем ответственность за их перемещение в допустимый поток до завершения рабочего потока?


person Alexander Kondratskiy    schedule 21.03.2011    source источник
comment
Это хороший вопрос. Вы проверили это так, как предложил @Troubadour?   -  person Martin Hennings    schedule 01.08.2013


Ответы (6)


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

Напомним, что QThread — это не поток, он управляет потоком.

Когда QThread заканчивается, он продолжает существовать, и объекты, которые в нем живут, продолжают жить в нем, но они больше не обрабатывают события. QThread можно перезапустить (не рекомендуется), после чего обработка событий возобновится (поэтому тот же QThread может управлять другим потоком).

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


Допустим, я вызываю QtConcurrent::run(), который запускает функцию в рабочем потоке, и в этой функции я динамически выделяю несколько QObject (для последующего использования). Поскольку они были созданы в рабочем потоке, их привязка к потоку должна быть такой же, как у рабочего потока. Однако, как только рабочий поток завершается, привязка потока 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, не имеют циклов событий.

person Oktalist    schedule 13.10.2016
comment
Когда QThread уничтожается, объекты, которые в нем жили, перестают иметь какое-либо сходство с потоком. - это задокументировано где-нибудь в документах? Если это задокументировано, означает ли это, что пока объект QThread существует, но его поток остановлен, мне не разрешено удалять объект (поскольку объекты могут быть удалены только в потоке их сходства), но после этого я могу удалить объект? - person Johannes Schaub - litb; 13.10.2016
comment
@JohannesSchaub-litb На самом деле в документах говорится, что мы не должны оставлять объекты в удаленном QObject, поэтому я отредактировал свой ответ. Об удалении объектов: объекты можно удалять из-за пределов потока их сходства, при условии, что вы можете гарантировать, что они в настоящее время не обрабатывают события, чего не будет, если QThread завершится. Но обычный способ — подключить QThread::finished к QObject::deleteLater. - person Oktalist; 13.10.2016
comment
А, понятно, спасибо. Текст, на который вы ссылаетесь, разъясняет это. И текст в документах ~QObject вводит в заблуждение, поскольку он говорит, что вы не должны удалять QObject напрямую, если он существует в потоке, отличном от того, который выполняется в данный момент. . - person Johannes Schaub - litb; 13.10.2016

Однако, как только рабочий поток завершается, привязка потока QObject больше не должна быть действительной.

Рабочий поток НЕ завершается после вызова вашей функции. Весь смысл использования QtConcurrent::run заключается в выполнении большого количества небольших задач на глобальном пул потоков (или некоторые предоставленные QThreadPool), в то время как повторное использование потоков, чтобы избежать накладных расходов на создание и уничтожение потоков для каждой из этих небольших задач. Помимо распределения вычислений по всем доступным ядрам.

Вы можете попробовать просмотреть исходный код Qt, чтобы увидеть как реализовано QtConcurrent::run. Вы увидите, что он в конечном итоге вызывает RunFunctionTaskBase::start , который фактически вызывает QThreadPool::start с QRunnable, который вызывает функция, которая изначально была передана в QtConcurrent::run.

Теперь я хочу перейти к тому, что QThreadPool::start это реализуется добавлением QRunnable в очередь, а затем попыткой разбудить один из потоков из пула потоков (которые являются ожидание добавления нового QRunnable в очередь). Здесь следует отметить, что потоки из пула потоков не запускают цикл событий (они не предназначены для этого), они просто выполняют QRunnable в очереди и не более того (они реализованы таким образом для причины производительности очевидно).

Это означает, что в тот момент, когда вы создаете QObject в функции, выполняемой в QtConcurrent::run, вы просто создаете QObject, который живет в потоке без цикла событий, из документы, ограничения включают:

Если цикл событий не запущен, события не будут доставлены в объект. Например, если вы создаете объект QTimer в потоке, но никогда не вызываете exec(), QTimer никогда не выдаст свой сигнал timeout(). Позвонить deleteLater() тоже не получится. (Эти ограничения применяются и к основному потоку.)


TL;DR: QtConcurrent::run запускает функции в потоках из глобального QThreadPool (или предоставленного). Эти потоки не запускают цикл событий, они просто ждут запуска QRunnables. Таким образом, QObject, живущий в потоке из этих потоков, не получает никаких событий.


В документации они использовали < a href="https://doc.qt.io/qt-5/threads-technologies.html#qthread-low-level-api-with-Optional-event-loops" rel="nofollow">QThread (возможно, с циклом событий и рабочим объектом) и используя QtConcurrent::run как две отдельные многопоточные технологии. Они не предназначены для смешивания вместе. Итак, в пулах потоков нет рабочих объектов, это просто напрашивается на неприятности.

Вопрос: Qt автоматически перемещает объекты QObject в родительский поток, или мы несем ответственность за перемещение их в допустимый поток до завершения рабочего потока?

Я думаю, что, посмотрев на вещи таким образом, ответ очевиден, что Qt НЕ автоматически перемещает QObjects в любой поток. Документация предупреждает об использовании QObject в QThread без цикла обработки событий, вот и все.

Вы можете свободно перемещать их в любую ветку, которая вам нравится. Но имейте в виду, что moveToThread() иногда может вызывать проблемы. Например, если перемещение рабочего объекта включает перемещение QTimer:

Обратите внимание, что все активные таймеры объекта будут сброшены. Таймеры сначала останавливаются в текущем потоке и перезапускаются (с тем же интервалом) в targetThread. В результате постоянное перемещение объекта между потоками может откладывать события таймера на неопределенное время.


Вывод: я думаю, что вам следует подумать об использовании собственного QThread, который запускает свой цикл обработки событий, и создать там свои рабочие QObject вместо использования QtConcurrent. Этот способ намного лучше, чем перемещение QObject, и позволяет избежать многих ошибок, которые могут возникнуть при использовании вашего текущего подхода. Взгляните на сравнительную таблицу многопоточных технологий в Qt и выберите технологию, наиболее подходящую для вашего варианта использования. Используйте QtConcurrent только в том случае, если вы хотите просто выполнить функцию с одним вызовом и получить ее возвращаемое значение. Если вы хотите постоянно взаимодействовать с потоком, вам следует переключиться на использование собственного QThread с рабочими QObjects.

person Mike    schedule 14.10.2016

Qt автоматически перемещает объекты QObject в родительский поток или мы несем ответственность за их перемещение в действительный поток до того, как рабочий поток завершится?

Нет, 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.

person Nikita    schedule 14.10.2016
comment
Привет, @JohannesSchaub-litb, пожалуйста, ознакомься с моим расследованием. надеюсь будет полезно? - person Nikita; 14.10.2016
comment
Что насчет удаления QThread? Мое расследование показало, что QObject, которые жили в нем, удовлетворят thread()==nullptr, но документы предполагают, что мы не должны полагаться на это. - person Oktalist; 14.10.2016
comment
@Окталист Верно. ~QThread устанавливает thread поле к 0 своего QThreadData объекта в строке d->data->thread = 0;. Из-за этого у каждого QObject, жившего в этом потоке, d->threadData->thread будет равно 0. Это означает thread() == nullptr. - person Nikita; 14.10.2016
comment
Спасибо за ваше расследование. Я хотел бы дать вам обоим половину награды, но, к сожалению, это было невозможно. - person Johannes Schaub - litb; 20.10.2016

Хотя это старый вопрос, я недавно задал тот же вопрос и просто ответил на него, используя QT 4.8 и некоторое тестирование.

Насколько я знаю, вы не можете создавать объекты с родителем из функции QtConcurrent::run. Я пробовал следующие два способа. Позвольте мне определить блок кода, затем мы изучим поведение, выбрав POINTER_TO_THREAD.

Какой-то псевдокод покажет вам мой тест

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 запускает метод, мне было бы интересно это услышать!

person user2025983    schedule 30.10.2013
comment
Вы можете использовать QThread::currentThread() в своей функции doWork, чтобы получить QThread, в котором QtConcurrent запускает метод. Но я не думаю, что это принесет какую-либо пользу, поскольку этот объект QThread живет в основном потоке (вы не можете сделать что-то вроде new QObject(QThread::currentThread()); ). - person Mike; 14.10.2016

Я не уверен, что Qt автоматически изменяет привязку к потоку. Но даже если это так, единственный разумный поток, к которому можно перейти, — это основной поток. Я бы сам нажал их в конце функции с резьбой.

myObject->moveToThread(QApplication::instance()->thread());

Теперь это имеет значение только в том случае, если объекты используют процесс обработки событий, такой как отправка и получение сигналов.

person Stephen Chu    schedule 21.03.2011
comment
Спасибо за ответ. Я знаю, как переместить объект в другой поток, мне просто любопытно, нужно ли это делать, и как Qt обрабатывает описанную выше ситуацию. Кроме того, вы можете переместить их в другой поток, помимо основного потока. Наличие явного вызова moveToThread внутри функции ограничило бы ее повторное использование за пределами этой ситуации. Я попытаюсь создать класс-оболочку вокруг QtConcurrent::run(), чтобы справиться с этими вещами и посмотреть, как это пойдет. - person Alexander Kondratskiy; 25.03.2011

Хотя документы Qt, похоже, не определяют поведение, вы можете узнать, отслеживая, что QObject::thread() возвращает до и после завершения потока.

person Troubadour    schedule 21.01.2012