Russian Qt Forum

Qt => Многопоточное программирование, процессы => Тема начата: sergek от Март 15, 2021, 09:01



Название: [Решено] Thread tried to wait on itself
Отправлено: sergek от Март 15, 2021, 09:01
Коллеги,
в одной библиотеке встретил любопытный способ использования QThread, попробовал на примере, приведенном в справке Qt (https://doc.qt.io/qt-5/qthread.html (https://doc.qt.io/qt-5/qthread.html)). В примере перенос объекта Worker в поток осуществляется в конструкторе Controller с ожиданием его завершения в деструкторе:

Код
C++ (Qt)
Controller::Controller(QObject *parent) : QObject(parent)
{
   worker = new Worker;
   worker->moveToThread(&workerThread);
   workerThread.start();
}
Controller::~Controller() {
   workerThread.quit();
   workerThread.wait();
   worker->deleteLater();
}
 
А в библиотеке, о которой я упоминал, это делается в самом Worker:

Код
C++ (Qt)
Worker::Worker(QObject *parent) : QObject(parent)
{
   workerThread = new QThread;
   moveToThread(workerThread);
   workerThread->start();
}
 
Worker::~Worker() {
   workerThread->quit();
   workerThread->wait();
   workerThread->deleteLater();
}

Но при этом формируется предупреждение "QThread::wait: Thread tried to wait on itself". Это сообщение понятно: ждать в себе завершения самого себя бессмысленно)) Но в чем принципиальная ошибочность моего кода?
В присоединенном архиве - пример второго способа.


Название: Re: Thread tried to wait on itself
Отправлено: ssoft от Март 15, 2021, 11:42
ждать в себе завершения самого себя бессмысленно)).

Это ж и есть ответ на вопрос.

Вызов деструктора Worker производится в активности потока workerThread, который и предлагается подождать с помощью workerThread->wait().
В первом же варианте вызов деструктора Controller производится в активности другого (скорее всего, главного) потока, связанного с объектом Controller. Здесь можно и подождать workerThread->wait().


Название: Re: Thread tried to wait on itself
Отправлено: sergek от Март 15, 2021, 12:22
Собственно, это всего лишь предупреждение, и wait в этом случае ничего не делает. Убираем wait, остается выход из циклов обработки событий quit и отложенное удаление объекта QThread с помощью deleteLater. Вопрос остается - это правильно?


Название: Re: Thread tried to wait on itself
Отправлено: Igors от Март 15, 2021, 12:44
Собственно, это всего лишь предупреждение, и wait в этом случае ничего не делает. Убираем wait, остается выход из циклов обработки событий quit и отложенное удаление объекта QThread с помощью deleteLater. Вопрос остается - это правильно?
Может избавиться от сообщения так
Код
C++ (Qt)
Worker::~Worker() {
   workerThread->quit();
   if (QThread::currentThread() != workerThread)
    workerThread->wait();
   workerThread->deleteLater();
}


Название: Re: Thread tried to wait on itself
Отправлено: ssoft от Март 15, 2021, 12:49
Собственно, это всего лишь предупреждение, и wait в этом случае ничего не делает. Убираем wait, остается выход из циклов обработки событий quit и отложенное удаление объекта QThread с помощью deleteLater. Вопрос остается - это правильно?

Удаление объекта QThread само по себе не завершает активность (Note that deleting a QThread object will not stop the execution of the thread it manages.).
Метод wait же ожидает завершения активности.

В общем случае, второй вариант допускает "подвешивание" незавершенных активностей, особенно если они реализованы в виде бесконечных циклов (когда реализована собственная версия метода MyThread::run() ).


Название: Re: Thread tried to wait on itself
Отправлено: ssoft от Март 15, 2021, 12:54
Всё даже может быть намного хуже, если полностью прочитать документацию)).
Цитировать
Note that deleting a QThread object will not stop the execution of the thread it manages. Deleting a running QThread (i.e. isFinished() returns false) will result in a program crash. Wait for the finished() signal before deleting the QThread.


Название: Re: Thread tried to wait on itself
Отправлено: sergek от Март 15, 2021, 12:57
Может избавиться от сообщения так
Именно так и реализовано в Qt ))


Название: Re: Thread tried to wait on itself
Отправлено: sergek от Март 15, 2021, 13:01
Всё даже может быть намного хуже, если полностью прочитать документацию)).
Цитировать
Note that deleting a QThread object will not stop the execution of the thread it manages. Deleting a running QThread (i.e. isFinished() returns false) will result in a program crash. Wait for the finished() signal before deleting the QThread.
Если не ошибаюсь, это предупреждение об прямом удалении с помощью delete. Я ж использую deleteLater. Правда, глубоко не залезал в реализацию, а там стоит wait))
Так можно или нельзя и почему?


Название: Re: Thread tried to wait on itself
Отправлено: ssoft от Март 15, 2021, 13:11
Вот пример по шагам

Код
C++ (Qt)
// деструктор вызывается в потоке workerThread
Worker::~Worker() {
   // в потоке workerThread обращаемся к объекту главного потока workerThread
   // (возможны проблемы с конкурентным доступом) с командой завершения QEventLoop
   workerThread->quit();
 
   // в потоке workerThread обращаемся к объекту главного потока workerThread
   // (возможны проблемы с конкурентным доступом) с командой ожидания завершения активности данного потока
   // получаем игнорирование команды и не завершенный поток workerThread
   workerThread->wait();
 
   // в очередь главный поток отправляем событие удаления объекта workerThread,
   // которое может быть обработано в главном потоке даже до завершения этого деструктора,
   // когда активность потока workerThread фактически не завершена.
   workerThread->deleteLater();
}


Название: Re: Thread tried to wait on itself
Отправлено: Авварон от Март 15, 2021, 13:31
Но в чем принципиальная ошибочность моего кода?

Так у вас телега впереди лошади.
QThread - это объект-хэндл треда, а не сам тред (сам тред - это то, что в run()). Хэндл нужен для управления тредом из родительского (часто главного) треда - родительский поток запускает новый поток, он же его останавливает и дожидается.
А так у вас получается что владелец потока (объект Worker) перемещен внутрь этого потока, а тред не может заджойнить сам себя.


Название: Re: Thread tried to wait on itself
Отправлено: sergek от Март 15, 2021, 14:00
Хэндл нужен для управления тредом из родительского (часто главного) треда - родительский поток запускает новый поток, он же его останавливает и дожидается.
Спасибо за напоминание, что "хэндл" живет в потоке, в котором он создан)) Но откуда следует, что остановить поток (quit) можно только из того потока, откуда он был запущен?


Название: Re: Thread tried to wait on itself
Отправлено: Авварон от Март 15, 2021, 14:34
Спасибо за напоминание, что "хэндл" живет в потоке, в котором он создан)) Но откуда следует, что остановить поток (quit) можно только из того потока, откуда он был запущен?

Ни откуда, quit() можно звать из любого потока.
Нельзя звать wait() из потока потому что, по сути, wait() - это join() - вы ждете когда две нитки сольются в одну (ту, которая вызвала wait()/join()). Как нитка может поджойнится сама с собой?


Название: Re: Thread tried to wait on itself
Отправлено: sergek от Март 15, 2021, 14:53
Иначе говоря, код
Код
C++ (Qt)
Worker::~Worker() {
   workerThread->quit();
   workerThread->deleteLater();
}
 
корректен?


Название: Re: Thread tried to wait on itself
Отправлено: ssoft от Март 15, 2021, 15:12
Спасибо за напоминание, что "хэндл" живет в потоке, в котором он создан)) Но откуда следует, что остановить поток (quit) можно только из того потока, откуда он был запущен?

Это QObject, с которым могут связаны какие-нибудь сигналы, и слоты вызываются в основном потоке. Сама обертка QThread не помечена, как thread-safe, таким образом одновременный вызов методов из разных потоков может привести к плачевным результатам.

Иначе говоря, код
Код
C++ (Qt)
Worker::~Worker() {
   workerThread->quit();
   workerThread->deleteLater();
}
 
корректен?

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


Название: Re: Thread tried to wait on itself
Отправлено: Igors от Март 15, 2021, 15:35
Иначе говоря, код
Код
C++ (Qt)
Worker::~Worker() {
   workerThread->quit();
   workerThread->deleteLater();
}
 
корректен?
Это не так уж важно :), плохо уже то что создается почва для (ненужных) раздумий. Вызов quit останавливает loop нитки, но нитка еще не завершилась.
Цитировать
Since Qt 4.8, if deleteLater() is called on an object that lives in a thread with no running event loop, the object will be destroyed when the thread finishes.
Т.е. если вызов был из workerThread - сработает. А вот из др - хз. Напр вызывающий вытеснен между quit и deleteLater, а когда проснулся - нитка уже завершена  

Edit: а, так из другой дкструктор вызваться и не сможет 


Название: Re: Thread tried to wait on itself
Отправлено: Авварон от Март 15, 2021, 16:13
корректен?

Нет, потому что тред незаджойнен, а незаджойненые треды ведут к крашам (когда main() уже вышел, Глобал статики развалились, а тред еще жужжит)


Название: Re: Thread tried to wait on itself
Отправлено: sergek от Март 15, 2021, 20:10
Все понял, признаю свои ошибки, больше не буду))


Название: Re: Thread tried to wait on itself
Отправлено: Igors от Март 16, 2021, 11:54
Нет, потому что тред незаджойнен, а незаджойненые треды ведут к крашам (когда main() уже вышел, Глобал статики развалились, а тред еще жужжит)
Вот есть желание сделать какого-то worker'а, который по собственной инициативе (может быть) запускает workerThrеad (возможно член класса), ну и завершает/удаляет ее в деструкторе. Что тут плохого/некорректного ?

Правда возникает проблемка как дождаться завершения workerThrеad не имея к ней доступа в main нитке


Название: Re: Thread tried to wait on itself
Отправлено: sergek от Март 16, 2021, 21:38
Правда возникает проблемка как дождаться завершения workerThrеad не имея к ней доступа в main нитке
Такой вокер имеет место на существование с единственным ограничением - его удаление должно выполняться в другом потоке, т.е. не в том, где создавался Worker и workerThread. Тогда текущий поток и поток workerThread не совпадают и wait() работает.
У меня это невозможно, к сожалению.


Название: Re: Thread tried to wait on itself
Отправлено: Igors от Март 17, 2021, 11:17
Такой вокер имеет место на существование с единственным ограничением - его удаление должно выполняться в другом потоке, т.е. не в том, где создавался Worker и workerThread. Тогда текущий поток и поток workerThread не совпадают и wait() работает.
У меня это невозможно, к сожалению.
А как удалить worker'а из main если worker->thread() == workerThread ? Если ничего не путаю, получим ошибку выполнения (и это правильно)

Проблемка не выглядит ужасной, можно напр взводить футуру по сигналу finished (workerThread), а в main ждать на этой футуре. С др стороны затея "worker сам разбирается со своими нитками" имеет смысл


Название: Re: Thread tried to wait on itself
Отправлено: sergek от Март 17, 2021, 14:45
А как удалить worker'а из main если worker->thread() == workerThread ?
Да никак. Controller и Worker в моем примере создаются и удаляются в main, из-за чего и проблема.
Но можно добавить еще один вспомогательный поток, который должен служить только для одного - при завершении программы удалить все worker'ы. Тогда описанный подход должен сработать.
Нет времени проверять))

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


Название: Re: Thread tried to wait on itself
Отправлено: Igors от Март 18, 2021, 08:37
Но можно добавить еще один вспомогательный поток, который должен служить только для одного - при завершении программы удалить все worker'ы. Тогда описанный подход должен сработать.
Зачем же нитками так разбрасываться? Можно напр ждать на футуре или семафоре, может оформить выход из главного цикла в 2 этапа, первый удаление workке'ов


Название: Re: [Решено] Thread tried to wait on itself
Отправлено: sergek от Март 19, 2021, 18:41
Чтобы тема приобрела какой-то завершенный вид, я доработал пример, добавив в него вспомогательный класс WorkerPool и поток poolThread для него. Класс служит для хранения указателей на worker'ы в виде простого списка и их удаления в своем потоке (чтобы wait сработал, как надо). Здесь создаются 2 worker'а:
Код
C++ (Qt)
Controller::Controller(QObject *parent) : QObject(parent)
{
   WorkerPool* pool = new WorkerPool;
   connect(&poolThread, &QThread::finished, pool, &QObject::deleteLater);
   pool->moveToThread(&poolThread);
   poolThread.start();
 
   for(int i=0; i<2; i++) {
       Worker *worker = new Worker(i);
       pool->append(worker);
 
       connect(this, &Controller::operate, worker, &Worker::doWork);
       connect(worker, &Worker::resultReady, this, &Controller::handleResults);
   }
 
 
   connect(&timer, &QTimer::timeout, [this]() { emit operate("do it"); });
}

Удаление pool выполняется по сигналу завершения вспомогательного потока finished, поток завершается в деструкторе контроллера:
Код
C++ (Qt)
Controller::~Controller() {
   poolThread.quit();
   poolThread.wait();
}

В деструкторе пула удаляются worker'ы оператором delete (это необходимо, чтобы вызвать деструктор worker'а в потоке пула):
Код
C++ (Qt)
WorkerPool::~WorkerPool() {
   for(auto worker: pool) {
       delete worker;
   }
}

Вот такой вывод этого примера (в начале каждого сообщения - идентификатор потока:
Код:
0x33E8 pool created
0x33E8 worker 1 created
0x33E8 worker 2 created
start work
0x0ef4 do it 1: done
0x3e78 do it 2: done
0x0ef4 do it 1: done
0x3e78 do it 2: done
0x0ef4 do it 1: done
0x3e78 do it 2: done
end work
0x3BBC worker 1 deleted
0x3BBC worker 2 deleted
0x3BBC pool deleted

Напомню, что идея не моя)) Если интересно, то первоисточник тут: https://github.com/StefanFrings/QtWebApp/blob/master/QtWebApp/httpserver/httpconnectionhandler.cpp (https://github.com/StefanFrings/QtWebApp/blob/master/QtWebApp/httpserver/httpconnectionhandler.cpp)
Там не все очевидно, автор мне подсказал, что удаление выполняется в HttpConnectionHandlerPool::cleanup().