Search code examples
windowsmultithreadingqtthreadpool

Qt multi-thead console application hangs on exit (using ctrl-c)


I'm writing a simple console application that spawns some workers using a QThreadPool. I'm compiling on Windows 10, using Qt 5.12.2 and Microsoft Visual C++ Compiler 14.0 (amd64). The work needs to start at regular intervals using a timer, but the work takes longer than the interval. 8 threads should be able to keep up with the work load. I start the application and it works very smoothly and as expected.

When I want to exit the command line application, I hit ctrl-c to stop. The application then hangs. The Timer stops and all output stops, but it doesn't return me to the command prompt. I have to open task manager to quit the application. I'm sure it has something to do with the QThreadPool not being cleaned up correctly, but I can't find how to clean it up. Thanks for the suggestions.

I tried catching the destroy signal from the timer and the aboutToQuit signal from the application. Neither one fire. If you comment out the start of the thread pool the application quits correctly. I also created the thread pool as a member variable in the MyTimer class and converted the worker to a pointer. All variations resulted in the same result, hang on exit.

I don't have a debugger on this machine to attach and see where the application hangs.

mytimer.h

#ifndef MYTIMER_H
#define MYTIMER_H

#include <QDebug>
#include <QTimer>
#include <QThreadPool>

class MyWorker : public QRunnable
{
public:
    void run()
    {
        QThread::msleep(150);  //simulate some work
        qDebug() << ".";
    }
};

class MyTimer : public QObject
{
    Q_OBJECT
public:
    MyTimer()
    {
        QThreadPool::globalInstance()->setMaxThreadCount(8);
        worker.setAutoDelete(false);
        // setup signal and slot
        connect(&timer, SIGNAL(timeout()), this, SLOT(MyTimerSlot()));

        timer.setTimerType(Qt::PreciseTimer);
        // msec
        timer.start(50);
    }

    QTimer timer;
    MyWorker worker;

public slots:
    void MyTimerSlot()
    {
        //Comment the below line and the ctrl-c will work.
        QThreadPool::globalInstance()->start(&worker);
        qDebug() << "-";
    }
};


#endif // MYTIMER_H

main.cpp

#include <QCoreApplication>
#include "mytimer.h"


int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    MyTimer timer;
    return a.exec();
}

I expect when I hit ctrl-c while the application is executing, the application will exit cleanly and return control to the command prompt.


Solution

  • You have two problems here:

    1. When the application is killed with Ctrl-C it receives a Kill or Abort signal from the Os and it is terminated asap. Therefore no destructors or aboutToQuit signals will be fired as a consequence of the interruption of normal stack unwinding.

    2. Your worker class does not handle interruption. Usually a run function has some sort of loop that iterate the work on some chunk of data. In order to stop gracefully the QRunnable you have to exit from the run function. You can do it thread safely using a std::atomic boolean member variable to break the loop into the MyWorker class and using signal/slots to toggle it.

    Here is the fix for problem 1:

    #include <QCoreApplication>
    #include <csignal>
    #include "mytimer.h"
    
    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
    
        MyTimer timer;
    
        QObject::connect(qApp, &QCoreApplication::aboutToQuit, &timer, &MyTimer::workerStopRequested);
    
        signal(SIGTERM, [](int sig) { qApp->quit(); });
        signal(SIGABRT, [](int sig) { qApp->quit(); });
        signal(SIGINT, [](int sig) { qApp->quit(); });
        signal(SIGKILL, [](int sig){ qApp->quit(); });
    
        return a.exec();
    }
    

    Using the utilities provided in the csignal header file you can catch the killing signal and force the application to quit, firing the aboutToQuit signal.

    This signal is also used to tell the MyTimer instance to stop its workers firing the workerStopRequested signal, that is part of the solution to problem 2:

    #ifndef MYTIMER_H
    #define MYTIMER_H
    
    #include <QDebug>
    #include <QTimer>
    #include <QThreadPool>
    
    class MyWorker : public QObject, public QRunnable
    {
        Q_OBJECT
    public:
        explicit MyWorker(QObject* parent = nullptr) :
            QObject(parent),
            QRunnable()
        {
            aborted = false;
        }
    
        void run()
        {
            while (!aborted)
            {
                QThread::msleep(150);  //simulate some work
                qDebug() << ".";
            }
        }
    
    public slots:
        void abort()
        {
            aborted = true;
            qDebug() << "stopped.";
        }
    
    protected:
        std::atomic<bool> aborted;
    };
    
    class MyTimer : public QObject
    {
        Q_OBJECT
    public:
        MyTimer()
        {
            QThreadPool::globalInstance()->setMaxThreadCount(8);
            worker.setAutoDelete(false); // <-- Good. Worker is not a simple QRunnable anymore
    
            // setup signal and slot
            connect(&timer, SIGNAL(timeout()), this, SLOT(MyTimerSlot()));
    
            connect(this, &MyTimer::workerStopRequested, &worker, &MyWorker::abort);
    
            timer.setTimerType(Qt::PreciseTimer);
            // msec
            timer.start(50);
    
        }
    
        QTimer timer;
        MyWorker worker;
    
    signals:
        void workerStopRequested();
    
    public slots:
        void MyTimerSlot()
        {
            //Comment the below line and the ctrl-c will work.
            QThreadPool::globalInstance()->start(&worker);
            qDebug() << "-";
        }
    };
    
    #endif // MYTIMER_H
    

    The MyWorker class inherits from QObject to exploit signal/slots to exit from the run function processing loop. The boolean "aborted" is an atomic variable to guarantee that it is accessed thread-safely. (Atomics are a c++11 feature)

    It is set to false after the workerStopRequested signal is emitted (see main.cpp) and the abort() slot is executed, as a consequence of the connection made in the constructor of MyTimer class.

    Notice that when aborted is toggled, it will cause the processing loop to stop at the next iteration. This means that you could see at most 8 dots printed on screen after the "stopped" string (one for each thread according to max thread count).