Открыть список
Как стать автором
Обновить

Комментарии 36

Спасибо за популяризацию данной темы и детальное описание "кишков"! А то, увы, на поверку оказывается, что большинство "разработчиков на куте" совершенно не умеют в потоки и высшим пилотажем оказывается наследование от QThread с переопределением run, а то, так и более убойные перлы типа obj->moveToThread(obj) и вариации этого...

Мне понравилось одно высказывание про `this->moveToThread(this)` — это как сначала закинуть удочку, а потом следом кинуть саму удочку (С)
Вообще, понимание данного механизма в настоящее время может пригодиться и вне Qt, например, при изучении Node.JS — там тоже есть свой цикл событий.

Писал, писал чего-то — много воды, примеров — мало, без примеров не так понятно. И главное таки не написал, как правильно работать с потоками. А именно, код исполняемого объекта должен создаваться в функции void QThread::run(). Если его создать в родительском потоке или даже в самом QThread, например:


class MyThread: public QThread 
{
    MyObjects obj;

protected:
    void run() override
    {
         obj.execute();
    }
}

То данные и стек этого объекта будут всё равно находиться в родительском потоке, т.е. обращение всё равно будет к данным из другого потока, что чревато крэшами. Правильное объявление:


class MyThread: public QThread 
{
protected:
    void run() override
    {
         MyObjects obj;
         obj.execute();
    }
}

Вот привёл бы парочку подобных примеров — уже было бы гораздо понятнее! ;)

Не обязательно создавать наследника QThread, чтобы поместить выполнение QObject в потоке, в большинстве случаев нужно использовать moveToThread. И в Qt есть более высокоуровневые классы для потоков чем QThread.
То данные и стек этого объекта будут всё равно находиться в родительском потоке

У объекта нет стека (стек — часть потока) и в приведенном примере если .execute() вызвано в потоке MyThread, то и стек будет находиться так же. Данные объекта в свою очередь не привязаны ни к одному потоку ибо лежат в «куче» (heap, говорящее название, не правда ли?). per se, чисто из того что написано в обоих примерах проблем нет.

Приведенный пример может крэшиться по другой причине — если MyObjects обрабатывают какие-то сигналы с данными которые меняет запущенный в другом потоке .execute. Но в этом случае второй пример (как показано) работать не будет, т.к. MyObjects просто не будут обрабатывать сигналы.

Как правильно:
MyObjects obj;
connect(this, startRunning, obj, execute)

QThread thread;
thread.run();

obj.moveToThread(&thread);
emit startRunning();

Это в предположении что MyObjects занимаются обработкой сигналов. Если нет и речь идет просто о запуске функции execute, то правильно
MyObjects obj;
QtConcurrent::run([&obj](){ obj.execute(); }); // assuming obj won't be destroyed soon


Вообще, как правило если Вы переопределяете QThread::run(), то Вы скорее всего делаете что-то неправильно.

Что-то мне кажется, что ваш "правильный" код будет вести себя крайне странно если выполнить его хотя бы два раза. Почему бы не сделать connect(thread, started, obj, execute)?

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

А так-то да, started вполне можно использовать в качестве startRunning. Если совсем канонически то остановка потоков и уничтожение объектов (оставленные в моем примере полностью за скобками) тоже вешается аналогичным образом на сигнал finished
QThread* thread = new QThread;
MyObjects * worker = new MyObjects ();
worker->moveToThread(thread);
connect(thread, started, worker, execute);
connect(worker, finished, thread, quit);
connect(worker, finished, worker, deleteLater);
connect(thread, finished, thread, deleteLater);
thread->start();

Здесь предполагается что execute() должен обязательно сделать emit finished().
Работать должен без проблем, или я Вашу идею не вполне понял.

У вас в процессе работы накапливаются соединения с событием startRunning. Второе выполнение кода приведёт к вызову execute у двух объектов сразу, третье вызовет вызов execute у трёх объектов.


Или я что-то путаю, и connect срабатывает только 1 раз? В таком случае я не вполне понимаю как им пользоваться в GUI.

Да, это верное замечание. Это не очень критично для реального кода — там обычно для старта используется некоторое существующее событие, а не создается специально свое новое и если объект создается и остается, то предполагается что он собственно и должен отрабатывать на каждое такое событие. Но в приведенном модельном примере да, надо либо гарантировать что MyObject будут существовать не более чем в одном экземпляре на создавший его инстанс, либо вручную делать disconnect у запустившегося потока, либо создавать по отдельному QThread на каждый объект и стартовать через сигнал started.

Ну, поскольку вы приводили свой пример как альтернативу наследованию от QThread — то очевидно, что правильным будет только последний вариант. Остальные варианты попросту решают другую задачу.

Насколько я понимаю (и насколько написано в вашем тексте же) QThread::run переопределить в случае, когда
— мы не пользуемся в данном thread signal/slot
— мы сделали какой-то свой механизм остановки
— мы сделали какой-то (свой) механизм синхронизации, чтобы этот run() не молотил бы непрерывно

Вроде бы — в таком случае все ок?
Вроде бы — в таком случае все ок?

В таком случае — да. Можно даже поддержку signal/slot реализовать сравнительно простыми усилиями. Но это определенное велосипедостроительство, «канонически» в Qt предполагается паттерн Actors в котором работу делают QObject-ы в ответ на сигналы.
В таком случае есть другая проблема: obj не может сам усыплять свой поток. А если логике не надо управлять потоком, то надо использовать greenthread и прочие очереди выполнения.
Такой странный вопрос: вы же все равно компилируете C++ современным компилятором? Есть ли какая нибудь особая причина использовать в Qt приложении именно QThread, а не std::thread?
Я почему-то последнее время все больше склоняюсь к тому, чтобы писать как можно больше std:: и как можно меньше Qt кода (только то, что иначе нельзя), даже в Qt приложении.
Есть ли какая нибудь особая причина использовать в Qt приложении именно QThread, а не std::thread?

Обработка очереди сообщений же.

На работе вечный спор между stdшниками и qtшниками. Первые с пеной у рта доказывают, что надо всё на std писать, а от qt использовать только gui. Но при это истинно удивляются, когда у них не работают нормально сигналы-слоты, или же приходится городить костыли, потому-что нет возможности использовать meta систему qt. Вторые доказывают, что раз пишешь на qt, то нужно использовать только qt. Чтобы по максимуму использовать всю мощь фреймворка, пусть даже в угоду переносимости (в случаях, когда хочется использовать код в другом проекте, без qt).
Как по мне, так метасистема Qt — зло. Я видимо к первым отношусь.

У вас это чисто идеологическая неприязнь или все-таки имеет под собой какую-то практическую основу? Например в случае тех же потоков метасистема Qt хороша уже хотя бы тем, что при правильном использовании практически полностью снимает вопрос безопасного взаимодействия между потоками, ибо сигналы/слоты — потокобезопасный инструмент by design.

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

Я не вижу прямо такого вот страшного конфликта между stl и qt. Серьезно пересекаются разве что только в контейнерах, но нет ни малейших проблем сделать QObject который будет хранить внутри себя stl-контейнер и предоставлять к нему наружу Qt-совместимый интерфейс.
И постоянно конвертировать QString в std::string и обратно…
У меня тоже проблема с использованием stl и qt типов. Все никак не могу провести границу между тем, где использовать qt, а где stl. Точнее, границу то провожу, но на этой границе получаются постоянные конвертирования туда-сюда. И что с этим делать, не понятно
QThread нужен только для QObject-ов и корректной работы у них сигналов и слотов «из коробки». Ну и появился он на 10 лет раньше и доступен на большем спектре платформ. Других причин предпочесть его std::thread нет, более того — Qt в принципе совместим и с std::thread.

Если вам не нужна обработка сообщений из коробки (а то может быть и вообще не нужна, и таких ситуаций миллион), то конечно причин нет.

Так назовите хоть одну реальную многопоточную альтернативу Qt signal-slot connection в std. И чтоб один и тот же код работал хотя бы в Linux и Windows, MacOS тоже приветствуется.

Уже даже и не спрашиваю про std::gui или std::sql.
Да, знаю, и в Qt шаг влево, шаг вправо, и прыжок на месте часто караются assertion или вообще acceess violation, а куда уйти то???
Вы просто привыкли к парадигме signal-slot и думаете без них обойтись нельзя.
Но по поводу кроссплатформного GUI тут соглашусь, особо выбора нет.
Справедливости ради, парадигма весьма удобна и изящна. К тому же в Qt5 она сводится только к «сигнал», сигналы подключаются к любым методам, не только к слотам.
> Вы просто привыкли к парадигме signal-slot и думаете без них обойтись нельзя.

юноша, я отлично программировал на Окнах. когда еще даже Borland не было

> Но по поводу кроссплатформного GUI тут соглашусь, особо выбора нет.

А что по поводу QtSql?
юноша, я отлично программировал на Окнах. когда еще даже Borland не было

Преклоняюсь: Windows 3.0 вышла в 1990, Borland C++ — в 1991. Правда, сама фирма Borland основана в 1981.., но это так…
Целый год отлично программировать на Win16 — это супер!
Преклоняюсь перед Вашим умением цитировать Вики и полном непонимании того, что был тогда Turbo Pascal и Turbo Vision, и почему Borland до Delphi не имел никакого отношения к созданию приложений под Windows
Строго говоря, имел — при установке BP7 в 3.11 наличествовала куча иконок для создания приложений в Win. Другое дело, что этим не пользовался никто, во всяком случае, я об этом не слышал.
И, по крайней мере, до 2014 Turbo Pascal + Turbo Vision были основными инструментами для обучения программированию первокурсников ВМК МГУ. У меня тогда не выдержало сердце смотреть на мучения сына в 640х480 окне, и я прикрутил ему TP к Notepad++, благо TP запускается с командной строки, в DOSBox.
> Строго говоря, имел — при установке BP7 в 3.11 наличествовала куча
> иконок для создания приложений в Win. Другое дело, что этим не
> пользовался никто, во всяком случае, я об этом не слышал.

Wow, вот уж не ожидал тут встретить кого-то, кто помнит Borland Pascal!
Однако, тогда идая OWC, или как оно тогда назывась, была не готова, не то что MFC годами чуть позже.

> И, по крайней мере, до 2014 Turbo Pascal + Turbo Vision были основными
> инструментами для обучения программированию первокурсников ВМК МГУ.

Что это за помoйка? Я уже в конце 90-ых преподавал Delphi
А что по поводу QtSql?

Если поверх него наваять обёртки, то можно жить.

Ага, все потоки cтавить в одну очередь и ждать, хотя реальные API давно уже многопоточные, только Qt-спецы об этом не знают ;)

Ну почему, можно создавать QSqlDatabase на каждый поток. Проблема в том, что надо SQL писать руками. Зато это стимулирует писать свой недоORM-фреймворк на темплейтах, а это весело!

Пока альтернативы Qt еще хуже, кьют будет жить. Это такая Java…
есть один весёлый момент в использовании:
так-как построение дерева объектов происходит в конструкторе исполняемом в главном потоке, то не привязанные к родителю объекты остаются в главном потоке.
такое случается когда забывают вставить ссылку на родителя в конструкторе или задействуют иные механизмы очистки. для однопоточного варианта не критично, а при переносе возникают проблемы.
и в qt-библиотеке случаются/случались/ конструкции не привязанные к родителю т.е. не переносимость в поток без доп ухищрений.

примерчик
{
	QThread *thread = QThread(this);
	QObjectExample* obj = new QObjectExample(nullptr);
	obj.moveToThread(thread)
}
QObjectExample::QObjectExample(QObject*)
{
	// где-то глубоко в исходниках:
	// кто-то писал для однопоточного кода 
	obj = new AnyQObject(/*или забыто this*/); // будет принадлежать главному потоку
}


Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.