2015-08-12 2 views
2

Когда я закрываю приложение, поток все еще работает, хотя он должен был закончиться. Следующий код просто зависает на линии workerThread->wait();.QThread не выйдет

Главная тема

#include "mainwindow.h" 
#include "ui_mainwindow.h" 

MainWindow::MainWindow(QWidget *parent) : 
    QMainWindow(parent), 
    ui(new Ui::MainWindow) 
{ 
    ui->setupUi(this); 

    workerThread = new QThread(); 
    worker = new Worker(); 

    worker->moveToThread(workerThread); 

    connect(this, SIGNAL(ThreadStopSignal()), worker, SLOT(ThreadStopSlot())); 
    connect(this, SIGNAL(StartWorkerSignal()), worker, SLOT(RunSlot())); 
    connect(worker, SIGNAL(finished()), workerThread, SLOT(quit())); 
    connect(worker, SIGNAL(finished()), worker, SLOT(deleteLater())); 
    connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater())); 

    workerThread->start(); 
    emit(StartWorkerSignal()); 
    qDebug()<<"Main thread: " << QThread::currentThreadId(); 
} 

MainWindow::~MainWindow() 
{ 
    qDebug() << "Asking threads to exit" << QThread::currentThreadId(); 
    emit(ThreadStopSignal()); 

    workerThread->wait(); 

    qDebug() << "thread is dead"; 
    delete ui; 
} 

реализация Worker

#include "worker.h" 

#include <QCoreApplication> 

Worker::Worker() 
{ 
    allowRun = true; 
} 

void Worker::RunSlot() 
{ 
    qDebug() << "Starting"; 
    int i = 0; 
    while(allowRun) 
    { 
     QThread::msleep(1000); 
     QCoreApplication::processEvents(); 
     qDebug() << i++ << QThread::currentThreadId(); 
    } 
    emit finished(); 
    qDebug() << "Done counting"; 
} 

void Worker::ThreadStopSlot() 
{ 
    allowRun = false; 
    qDebug() << "Ending HID WORKER" << QThread::currentThreadId(); 
} 

Типичный процесс будет производить следующий вывод (Qt 5.1.1 лязг x86_64) *:

Main thread: 0x7fff743b2300 
Starting 
0 0x10b6e1000 
1 0x10b6e1000 
2 0x10b6e1000 
3 0x10b6e1000 
4 0x10b6e1000 
5 0x10b6e1000 
6 0x10b6e1000 
7 0x10b6e1000 
8 0x10b6e1000 
9 0x10b6e1000 
10 0x10b6e1000 
11 0x10b6e1000 
Asking threads to exit 0x7fff743b2300 
Ending HID WORKER 0x10b6e1000 
12 0x10b6e1000 
Done counting 

Однако приложение все равно будет работать, без пользовательского интерфейса; он будет иногда сбой, в результате чего откроется диалоговое окно отчета о сбое Apple-аварии.

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

+0

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

ответ

1

Проблема довольно проста: вы блокируете цикл событий в основном потоке, когда вы заканчиваете wait. Тем не менее, в то же время цикл события должен получить вызов workerThread->quit(). Таким образом, вы зашли в тупик.

Простым исправлением является явно quit() нить до wait() для него. Вы достигаете этого, фиксируя присущую ему разлому QThread. Ниже приведена безопасная реализация класса Thread. Затем вы можете просто уничтожить поток в деструкторе MainWindow.

Увы, код имеет некоторые антипаттерны и вуду.

  1. Ни workerThread, ни worker должны быть указателями. Это преждевременная пессимизация, поскольку вы добавляете дополнительное распределение кучи и дополнительный слой косвенности. Это бессмысленно и заставляет вас делать ручное управление памятью, в котором никто не требует.

  2. threadStopSlot является потокобезопасным, и нет причин для его вызова из цикла событий рабочего потока. Вы можете назвать это напрямую. Когда вы это сделаете, вам не нужно звонить QCoreApplication::processEvents в runSlot. Когда вы это сделаете, вы снова вводите цикл событий, и вдруг все ваши объекты, работающие в этом потоке, подлежат повторному требованию и должны быть проверены как таковые. Плохая идея - не делай этого.

  3. Так как вы, вероятно, хотите, чтобы позволить запустить цикл обработки событий в потоке рабочего, вы должны инвертировать управление: вместо того, чтобы держать контроль в runSlot, держать его в курсе событий, и есть вызов цикла событий runSlot несколько раз. Это идиома таймера тайм-аута.

  4. Списки инициаторов дают начало идиоматическому C++. Используй их.

  5. emit предназначен как префикс, а не как функция. Вы emit fooSignal(), а не emit(fooSignal()). Это вопрос стиля, правда, но emit предназначен для документации. Это предназначено только для потребления человеком, и мы, люди, легче читаем вещи, когда они не завернуты в дополнительный слой круглых скобок.Если вам не нужен аспект документации, вообще не используйте emit. Сигналы являются регулярными методами с машинной реализацией. Вам не нужно называть их каким-либо особым образом.

  6. Поскольку вы используете Qt 5, вы должны использовать синтаксис connect с компиляцией. Для этого не требуется компилятор C++ 11 - если вы не используете lambdas.

  7. Возможно, это плохая идея попросить потоки выйти из деструктора окна, поскольку в главном потоке могут возникать другие вещи, поскольку рабочие потоки завершаются - вещи, которые требуют запуска цикла событий ,

    Единственный способ разрушения окна - если у него есть атрибут WA_DeleteOnClose или если вы закончили цикл основного события и разрушили окно при выходе из main(). Вы должны поймать событие закрытия окна и настроить движение, чтобы окно удалялось только тогда, когда все, что должно быть сделано, сделано.

  8. Вместо вывода идентификатора потока в qDebug() вы можете выводить сам поток. Вы можете использовать тот факт, что потоки являются объектами, и вы можете дать им читаемые человеком имена. Затем вам не нужно сравнивать идентификаторы потоков или адреса вручную, вы просто читаете имя потока.

Учитывая все вышесказанное, если вы попросили меня написать код, я бы сделал это следующим образом.

Во-первых, функциональность работник может быть абстрагированы в рабочий базе:

#include <QtWidgets> 

class WorkerBase : public QObject { 
    Q_OBJECT 
    Q_PROPERTY(bool active READ isActive WRITE setActive) 
    QBasicTimer m_runTimer; 
    bool m_active; 
protected: 
    void timerEvent(QTimerEvent * ev) { 
     if (ev->timerId() != m_runTimer.timerId()) return; 
     work(); 
    } 
    virtual void workStarted() {} 
    virtual void work() = 0; 
    virtual void workEnded() {} 
public: 
    WorkerBase(QObject * parent = 0) : QObject(parent), m_active(false) { 
     setActive(true); 
    } 
    /// This method is thread-safe. 
    bool isActive() const { return m_active; } 
    /// This method is thread-safe. 
    void setActive(bool active) { 
     QObject source; 
     QObject::connect(&source, &QObject::destroyed, this, [this,active]{ 
     // The functor is executed in the thread context of this object 
     if (m_active == active) return; 
     if (active) { 
      m_runTimer.start(0, this); 
      workStarted(); 
     } else { 
      m_runTimer.stop(); 
      workEnded(); 
     } 
     m_active = active; 
     }, thread() ? Qt::QueuedConnection : Qt::DirectConnection); 
    } 
    ~WorkerBase() { 
     Q_ASSERT(QThread::currentThread() == thread() || !thread()); 
     setActive(false); 
    } 
}; 

Затем работник становится простым:

class Worker : public WorkerBase { 
    Q_OBJECT 
    int m_runCount; 
protected: 
    void workStarted() Q_DECL_OVERRIDE { 
     qDebug() << "Starting" << QThread::currentThread(); 
    } 
    void work() Q_DECL_OVERRIDE { 
     QThread::msleep(1000); 
     ++ m_runCount; 
     qDebug() << m_runCount << QThread::currentThread(); 
    } 
    void workEnded() Q_DECL_OVERRIDE { 
     qDebug() << "Finishing" << QThread::currentThread(); 
     emit finished(); 
    } 
public: 
    Worker(QObject * parent = 0) : WorkerBase(parent), m_runCount(0) {} 
    Q_SIGNAL void finished(); 
}; 

Наконец, добавим реализацию поточно, и основное окно, которое удерживает локус управления в цикле событий и пропускает цикл до тех пор, пока все не будет готово к разрушению окна:

class Thread : public QThread { 
    using QThread::run; // final method 
public: 
    Thread(QObject * parent = 0) : QThread(parent) {} 
    ~Thread() { quit(); wait(); } 
}; 

class MainWindow : public QMainWindow { 
    Q_OBJECT 
    Worker m_worker; 
    Thread m_workerThread; 
    QLabel m_label; 
protected: 
    void closeEvent(QCloseEvent * ev) { 
     if (m_worker.isActive()) { 
     m_worker.setActive(false); 
     ev->ignore(); 
     } else 
     ev->accept(); 
    } 
public: 
    MainWindow(QWidget * parent = 0) : QMainWindow(parent), 
     m_label("Hello :)\nClose the window to quit.") 
    { 
     setCentralWidget(&m_label); 
     m_workerThread.setObjectName("m_worker"); 
     m_worker.moveToThread(&m_workerThread); 
     connect(&m_worker, &Worker::finished, this, &QWidget::close); 
     m_workerThread.start(); 
     qDebug() << "Main thread:" << QThread::currentThread(); 
    } 
    ~MainWindow() { 
     qDebug() << __FUNCTION__ << QThread::currentThread(); 
    } 
}; 

int main(int argc, char ** argv) 
{ 
    QApplication a(argc, argv); 
    QThread::currentThread()->setObjectName("main"); 
    MainWindow w; 
    w.show(); 
    w.setAttribute(Qt::WA_QuitOnClose); 
    return a.exec(); 
} 

#include "main.moc" 

Обратите внимание на заметное отсутствие множества сигналов/слотов, требуемых вашим подходом. Все, что вам нужно, - это когда рабочий объект закончил, вы можете закрыть окно и разорвать все объекты на клочки.

Обратите внимание, что заказ декларации важен. Семантика C++ не является случайной в этом отношении: порядок имеет смысл. Рабочий поток должен быть объявлен после рабочего объекта, так как он будет разрушен в обратном порядке. Таким образом, поток сначала заканчивается и уничтожается. В этот момент m_worker->thread() == nullptr и вы можете уничтожить работника из любого потока, включая основной поток. Было бы ошибкой, если бы поток работника все еще существовал - тогда вам нужно было бы уничтожить рабочего в своем потоке.

Выход:

Main thread: QThread(0x7fa59b501180, name = "main") 
Starting QThread(0x7fff5e053af8, name = "m_worker") 
1 QThread(0x7fff5e053af8, name = "m_worker") 
2 QThread(0x7fff5e053af8, name = "m_worker") 
3 QThread(0x7fff5e053af8, name = "m_worker") 
Finishing QThread(0x7fff5e053af8, name = "m_worker") 
~MainWindow QThread(0x7fa59b501180, name = "main") 
+0

Я бы предпочел не подклассифицировать QThread, если это вообще возможно. Это, как правило, плохая практика, связанная с тем, как работает QThread, и в этом случае приведет к тому, что ситуация сломает линию, в которой это будет реализовано позднее. – pdel

+0

@pdel 'QThread' не работает. Обычные объекты C++ могут быть уничтожены в любое время. QThread, по умолчанию, не всегда может быть. Поэтому вы должны это исправить. Именно так вы правильно реализуете шаблон C++ RAII.'QThread' не реализует его должным образом, и вы должны обойти его, явно вызывая' quit() 'и' wait() ', и это скрытая бомба, которая взорвется, как только вы забудете сделать это в некоторый путь кода. –

+0

@pdel Подкласс, который я показываю, - это не только хорошая практика, это абсолютно необходимо, а 'QThread' as - просто имеет давнюю ошибку. Вы не должны переопределять 'run' - вот почему он сделан окончательным. Вы не можете переопределить 'Thread :: run', даже если хотите. То, что вы видите, является последней идиомой, которую вы используете, если хотите отметить унаследованную реализацию как окончательную. –