Что происходит с сродством потока к объекту QObject, созданному в рабочем потоке, который затем завершается? - PullRequest
23 голосов
/ 21 марта 2011

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

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

Ответы [ 6 ]

6 голосов
/ 13 октября 2016

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 s, которая переживает задачу, является сомнительным проектом, задачи должны быть автономными. Как отмечает @Mike, QThread, используемые QtConcurrent::run, не имеют циклов событий.

5 голосов
/ 14 октября 2016

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

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

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

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

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

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


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


В документации Они поставили использование QThread (возможно, с циклом событий и рабочим объектом) и использование QtConcurrent::run в качестве две отдельные многопоточные технологии. Они не предназначены для смешивания. Итак, нет рабочих объектов в пулах потоков , это просто вызывает проблемы.

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

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

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

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


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

3 голосов
/ 14 октября 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.

2 голосов
/ 31 октября 2013

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

1 голос
/ 22 января 2012

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

1 голос
/ 22 марта 2011

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

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

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

...