Qt Невозможно переместить цель в поток

Я столкнулся со странной ошибкой в ​​своем приложении Qt 5.7 (в Windows 10), и обычных виновников такого поведения нигде не найти:

  • У перемещаемого объекта есть родитель - скорее всего, это не так
  • Попытка вытащить объект в поток вместо того, чтобы толкать его - это причина ошибки, однако я понятия не имею, откуда она берется

Полное сообщение об ошибке

QObject::moveToThread: Текущий поток (0x2afcca68) не является потоком объекта (0x34f4acc8). Невозможно перейти к целевому потоку (0x34f4adc8)

QObject::setParent: Невозможно установить родителя, новый родитель находится в другом потоке

и вот также мой код:

main.cpp

#include <QApplication>
#include <QQuickItem>
#include "CustomQuickWidget.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QApplication app(argc, argv);

    const QUrl source = QUrl(QLatin1String("qrc:/main"));
    CustomQuickWidget widget(source);

    return app.exec();
}

main (псевдоним для main.qml):

// You can put any random QML content in this case really as long as it doesn't create a window since the CustomQuickWidget does that.
Rectangle {
    id: window
    visible: true
    width: 600
    height: 480
}

CustomQuickWidget.cpp

#include "CustomQuickWidget.h"
#include <QQuickItem>

CustomQuickWidget::CustomQuickWidget(const QUrl &source, QWidget *parent) : QQuickWidget(source, parent) {
    // Setup the recognizer
    this->airWheelRecognizer = new QAirWheelGestureRecognizer();
    this->airWheelType = QGestureRecognizer::registerRecognizer(airWheelRecognizer);
    // and turn on grabbing for all the supported gestures
    grabGesture(airWheelType);
    grabGesture(Qt::SwipeGesture);
    grabGesture(Qt::TapGesture);

    // Create thread and device worker
    this->deviceThread = new QThread(this);
    this->deviceWorker = new DeviceMapper(this, Q_NULLPTR); // NOTE: this here is NOT for parent. The constructor's signature for this class is: DeviceMapper(QObject* receiver, QList<Qt::GestureType>* gestureIDs, QObject* parent = Q_NULLPTR)
    this->deviceWorker->init();

    // Create timer that will trigger the data retrieval slot upon timeout
    this->timer = new QTimer();
    this->timer->setTimerType(Qt::PreciseTimer);
    this->timer->setInterval(5);

    // Move timer and device mapper to other thread
    this->timer->moveToThread(this->deviceThread);
    this->deviceWorker->moveToThread(this->deviceThread); // FIXME For unknown reason: QObject::moveToThread: Current thread (...) is not the object's thread. Cannot move to target thread

    // Connect widget, timer and device mapper
    createConnections();

    // Run thread
    this->deviceThread->start();

    // Connect device and start data retrieval
    QTimer::singleShot(0, this->deviceWorker, &(this->deviceWorker->slotToggleConnection));
    QTimer::singleShot(0, this->deviceWorker, &(this->deviceWorker->slotToggleRun));

    this->show();
}

CustomQuickWidget::~CustomQuickWidget()
{
    if (this->deviceThread) {
        this->deviceThread->quit();
        this->deviceThread->wait();
    }
}

void CustomQuickWidget::createConnections()
{
    connect(this->timer, SIGNAL(timeout()),
            this->deviceWorker, SLOT(slotRetrieveData()));

    connect(this->deviceThread, SIGNAL(started()),
            this->timer, SLOT(start()));
    connect(this->deviceThread, SIGNAL(finished()),
            this->deviceWorker, SLOT(deleteLater()));
    connect(this->deviceThread, SIGNAL(finished()),
            this->deviceThread, SLOT(deleteLater()));
}

bool CustomQuickWidget::event(QEvent* event) {
    if (event->type() == QEvent::Gesture) { 
        bool res = gestureEvent(static_cast<QGestureEvent*>(event)); // Not important so not included as code here
        return res;
    }

    return QWidget::event(event);
}

Как вы можете видеть, у меня здесь происходит типичный рабочий поток. Я убедился, что у моего работника (здесь DeviceMapper) нет родителя. Он также создается внутри моего виджета (где также создается QThread), но перемещается в поток вместе с таймером.

Теперь, помимо очевидной проблемы, вынесенной в заголовок, я должен упомянуть следующее:

  • Нет такой ошибки при вызове this->timer->moveToThread(this->deviceThread);
  • Тот же самый код без проблем работает в другом проекте, который является поддиректорным проектом — один подпроект создает общую библиотеку (которую я также использую в этом проекте), а другой — приложение, использующее библиотеку.

Единственная разница между моим другим приложением и этим заключается в использовании QQuickWidget (вместо QWidget) и QML. Я новичок в QML, и это также мой первый QQuickWidget, поэтому я мог упустить некоторые очевидные настройки, которые необходимо «активировать».

я также добавил

cout << this->deviceWorker->thread()->currentThreadId() << endl;
cout << this->thread()->currentThreadId() << endl;

прямо перед this->deviceWorker->moveToThread(this->deviceThread); и я получил

0x18b0
0x18b0

это означает, что до moveToThread(...) мой объект принадлежит тому же потоку, где создается экземпляр QThread. Печать идентификатора потока после moveToThread(...) возвращает тот же результат, но это ожидается из-за невозможности правильного перемещения объекта в другой поток.


ОБНОВЛЕНИЕ:

Сообщение об ошибке появляется ТОЛЬКО при сборке в режиме выпуска, однако независимо от типа сборки ошибка все еще присутствует.


person rbaleksandar    schedule 09.08.2016    source источник
comment
QThread::currentThreadId() — это статическая функция, которая возвращает поток исполняемого в данный момент кода, а не поток, в котором находится объект. Просто отладьте QObject::thread(), чтобы узнать, в каком потоке находится объект.   -  person thuga    schedule 09.08.2016
comment
Ах хорошо. Сделает это сразу.   -  person rbaleksandar    schedule 09.08.2016
comment
Установите собственный обработчик сообщений через qInstallMessageHandler и установите там точку останова, а затем подождите, пока вы не получите это предупреждение от Qt. Затем посмотрите в стеке, чтобы проверить, какой QObject на самом деле испускает это. Это может быть какой-то подобъект deviceWorker.   -  person peppe    schedule 09.08.2016
comment
По предложению @thug: я проверил, и threadId остается 0. Что касается способа @peppe - проблема в том, что сообщение появляется только в режиме release, поэтому я не могу его отладить. Я где-то читал, что могу удалить флаг уровня оптимизации -O3 из сгенерированного Makefile, чтобы предотвратить удаление из двоичного файла всей важной отладочной информации, но я не знаю, работает ли это на самом деле.   -  person rbaleksandar    schedule 09.08.2016
comment
@rbaleksandar Нет, вы должны оставить тот же уровень оптимизации, чтобы отлаживать ту же программу ... но добавить -g, чтобы включить символы отладки. Тот факт, что поведение различается между уровнями оптимизации, указывает на то, что вы где-то вызываете UB, но этот режим отладки не применяет какую-то оптимизацию, которая это обнаруживает.   -  person underscore_d    schedule 09.08.2016
comment
Спасибо. Я добавил флаг как к CFLAGS, так и к CXXFLAGS (просто чтобы убедиться, что я ничего не пропустил, поэтому теперь у меня есть -pipe -fno-keep-inline-dllexport -ggdb -O2 -std=gnu++11 -frtti -Wall -Wextra -fexceptions -mthreads $(DEFINES). Однако я все еще не могу отлаживать. Я устанавливаю флаг в неправильном месте. Я' мы добавили это в файл Makefile.Release внутри моей библиотеки build, так как это тот, который вызывается из Makefile верхнего уровня при сборке релиза.   -  person rbaleksandar    schedule 09.08.2016
comment
@rbaleksandar Мой makefile также включает его в шаг ссылки, хотя я не совсем уверен, требуется ли это. Вы также должны убедиться, что двоичный файл не содержит stripсимволов, что сведет на нет все ваши усилия!   -  person underscore_d    schedule 09.08.2016
comment
Кстати, я знаю, что большая часть устаревших документов Qt использует его, что очень прискорбно, но все руководства new/delete везде обычно не нужны, открывают вас для несчастных случаев и не являются хорошим/современным стилем C++... который так же как и Qt, но в последнее время он, наконец, прилагает надлежащие усилия, если только все устаревшие документы и учебные пособия догонят. Я предлагаю изменить любой объект, который вам не требуется, чтобы пережить область, в которой он объявлен, чтобы иметь продолжительность автоматического/по значению хранения. Это может повлиять или не повлиять на это, но это просто хороший стиль и, вероятно, предотвратит различные другие возможные головные боли.   -  person underscore_d    schedule 09.08.2016
comment
@underscore_d Хорошо подмечено. У меня есть -s -Wl внутри LFLAGS, который делает именно это — удаляет из двоичного файла любые символы отладки. Я удалил его, и теперь отладчик срабатывает в режиме выпуска. Спасибо! Что касается new/delete, я стараюсь избегать этого, но когда это происходит, но здесь у меня нет выбора (я преобразовал элемент QThread в стек), поскольку и мой таймер, и deviceWorker должны быть динамически выделены. В противном случае, как только конструктор будет завершен, они выйдут за пределы области видимости, и все. :D   -  person rbaleksandar    schedule 09.08.2016
comment
@rbaleksandar Отлично, рад, что угадал! Дайте нам знать, как проходит сеанс отладки.   -  person underscore_d    schedule 09.08.2016
comment
@rbaleksandar both my timer and the deviceWorker need to be dynamically allocated. Otherwise once the constructor is done they will go out of scope and that's that. Но это неправда! Они члены класса. Единственное, что может выйти из области видимости в конце конструктора, — это локальные переменные, объявленные в его теле. Если вы зададите этим элементам автоматический срок хранения, они будут жить столько же, сколько и содержащий их объект.   -  person underscore_d    schedule 09.08.2016
comment
Вы либо вызываете не потокобезопасные методы DeviceWorker из неправильного потока (потоков), либо DeviceWorker владеет некоторыми объектами, которые не являются его дочерними элементами. В любом случае вы обязательно должны опубликовать полный пример, и он должен быть в одном файле. Совершенно бессмысленно иметь отдельные файлы заголовков в таком тестовом примере. Вы также должны продолжать удалять код до тех пор, пока ни один из них не сможет быть удален без исчезновения ошибки. Запихните все в main.cpp, оканчивающееся на #include "main.moc". См., например. этот ответ для идеи.   -  person Kuba hasn't forgotten Monica    schedule 09.08.2016
comment
@underscore_d Я бы сказал, что new нужен так же сильно или мало, как это когда-либо было с QObject и другими классами с семантикой родитель-потомок, а delete нужен так же мало, как когда-либо с ними. Это на самом деле совсем не изменилось с современным С++ (за исключением людей, которые действительно понимают, что им не нужно помещать все в кучу отдельно, они могут использовать переменные-члены и т. д.).   -  person hyde    schedule 16.08.2016
comment
@hyde Думаю, я думал о двух вещах: (1) современный стиль Qt, в котором (помимо меньшего количества макросов) люди понимают, что использование кучи гораздо менее необходимо, чем они думали - & than/ потому что в официальной документации указано! - все это время. И (2) современный C++ стиль/этос, согласно крупным блогерам и т. д., который больше касается использования, чем функций, но более новые стандарты предоставляют множество/еще более простых способов прислушаться к указанным советам. Постоянный совет, исходящий от # 2 - повышенный акцент на том, чтобы не управлять памятью вручную, используя интеллектуальные указатели, особенно. семантика значений и т. д. — вероятно, помогает людям понять, что они могут сделать № 1   -  person underscore_d    schedule 16.08.2016


Ответы (1)


Мне удалось решить мою проблему, определив, КОГДА это происходит.

В конце прошлой недели приложение, которое я писал, внезапно заработало, поэтому, хотя меня беспокоило, почему все это произошло до этого, я позволил этому быть. Я не менял ни код библиотеки (за исключением пары комментариев в моем коде, которые явно не могут повлиять на сам код), ни C++ код моего QML приложения. Все, что я изменил, это мой QML, но на самом деле это не имело отношения к коду C++ под ним. Единственное, что я изменил, это тип сборки. Однако на прошлой неделе я не заметил этого.

Вчера я начал работать над новым проектом. И сразу после первого запуска у меня возникла та же проблема. Это сводило меня с ума. Итак, я начал анализировать свой код (@Kuba Ober, извини, приятель, но опубликовать полный код или даже небольшой кусок библиотеки невозможно, иначе я бы это сделал (хотя это пара сотен строк реального кода ( исключая такие вещи, как комментарии и пустые строки)). Я проверил и перепроверил отношения родитель-потомок, но не смог найти ничего, что могло бы дать мне хотя бы небольшую подсказку, когда и почему это происходит. Я также проанализировал стек, чтобы изо всех сил, но все напрасно.

И тут меня осенило... Выше я упоминал, что мой предыдущий проект внезапно заработал после смены его типа сборки. И действительно, это было источником всего зла в моем положении. Я добавляю свою библиотеку в свои проекты (за исключением первоначальной, которая вместе с библиотекой является частью одного и того же проекта subdir) путем создания папки в корневом каталоге моего нового проекта с именем libs и копирования в нее связанных материалов. Теперь, когда я закончил работу над своей библиотекой и провел некоторое тестирование, я, очевидно, решил переключиться на релизную сборку. Однако я скопировал сборку библиотеки в режиме release в сборку проекта в режиме debug. Итак, после пары пересборок и копирования библиотеки тут и там я обнаружил, что смешивание типов сборки приложения, использующего библиотеку, и самой библиотеки приводит к этой проблеме.

Я знаю, что смешивание типов сборки — плохая идея, и обычно я этого не делаю, но на этот раз это просто вылетело из головы и произошло совершенно случайно. Я не знаю, что происходит внутри, когда приложение с типом сборки X и библиотека с типом сборки Y смешиваются, но результатом в моем случае была ошибка, которую я опубликовал в этой теме.

Спасибо за помощь. Я многому научился благодаря вашим комментариям! Несмотря на то, что в моем случае отладка не понадобилась, примите мою благодарность. :)

person rbaleksandar    schedule 16.08.2016
comment
Таким образом, даже если вы скомпилируете приложение и библиотеку с несоответствующими типами сборки, но противоположными наоборот, вы получите одинаковые результаты? Это может объяснить, но я беспокоюсь, что в равной степени это может быть связано с различными оптимизациями, которые попеременно маскируют/выявляют результаты UB. Я видел что-то, напоминающее то, что вы сказали, но это было потому, что я непреднамеренно скомпилировал свою программу с неправильным компилятором (смеется) и связал ее с библиотекой, скомпилированной правильным. Это я интуитивно понимаю. Несовместимых режимов сборки из одного и того же компилятора не так много. Но я могу ошибаться... Что думают наши специалисты по сборке? - person underscore_d; 16.08.2016
comment
Да, единственный сценарий, в котором все работает, - это когда их типы сборки совпадают, то есть dll (в отладке) + приложение (в отладке) и dll (в выпуске) + приложение (в выпуске) = нет проблем. Что касается точной почему проблемы, я написал, что не имею ни малейшего представления. Поскольку я любопытный человек, я хотел бы узнать об этом, но, учитывая, что я не могу предоставить код для библиотеки, я не вижу, что еще можно сказать об этой проблеме. Кстати, исходный проект subdir создает и приложение, и общую библиотеку с одним и тем же типом сборки, поэтому проблема не появлялась раньше. - person rbaleksandar; 16.08.2016