Worker class for multithreading in Qt

8k views Asked by At

I've been thinking about the worker class that can be used for parallel computing.

I wanted to avoid the situation when I have to move the worker to separate thread explicitly after creating the object i.e.:

Worker worker;
QThread thread;
worker.moveToThread(&thread);
thread.start();
worker.start();

This is the solution I came up with:

Header:

#include <QObject>
#include <QThread>

class ThreadedWorker : public QObject
{
    Q_OBJECT
public:
    explicit ThreadedWorker(QObject *parent = 0);
signals:
    void finished(ThreadedWorker* worker);
public slots:
    void start();
protected:
    virtual void run();
    QThread workerThread_;
    bool isRunning_;
};

Source:

#include "threadedworker.h"

ThreadedWorker::ThreadedWorker(QObject *parent) : QObject(parent)
{
    this->moveToThread(&this->workerThread_);
    this->workerThread_.start();
    this->isRunning_ = false;
}

void ThreadedWorker::start()
{
    if(!this->isRunning_)
     {
         this->isRunning_ = true;
         this->run();
     }
 }

 void ThreadedWorker::run()
 {
     // HERE GOES THE ACTUAL WORK FOR THE WORKER
     //
     //
     emit this->finished(this); // added to inform a controller object that the worker has finished
   }

UPDATED after comment by Zailborg:

So now I just create:

ThreadedWorker worker1;
ThreadedWorker worker2;

invoke their start() slots by some external signal and they run in parallel.

However my main concern is whether it is not a bad practice to put QThread workerThread_ as the member of ThreadedWorker class and moving the object to that thread in constructor.

3

There are 3 answers

1
TheDarkKnight On BEST ANSWER

whether it is not a bad practice to put QThread workerThread_ as the member of ThreadedWorker class

When an object is moved to its thread, the object and its children are moved. The children are the objects linked in the Qt parent child hierarchy when you pass a parent to a constructor of a QObject derived class, or call setParent.

In the case of a member pointer, such as a QThread pointer, it is not a "child" of the class. Therefore, when the ThreadedWorker object is moved to the new thread, this will work. However, problems can arise due to the confusion of thread affinity. The main object is moved to the the new thread, but holds a member pointer to an object whose thread affinity is different; the object pointed to by QThread*.

The code presented in the question doesn't refer to a QThread pointer, but a QThread instance. With this in mind, consider the documentation for QObject::moveToThread when it states: -

Warning: This function is not thread-safe; the current thread must be same as the current thread affinity. In other words, this function can only "push" an object from the current thread to another thread, it cannot "pull" an object from any arbitrary thread to the current thread.

So, I suspect the design of QThread and QObject::moveToThread expects that the affinity of QThread is stable and will not be altered. By being a member of the object being moved, this is not going to be the case.

For this reason, I would say it is not a good idea to have a QThread instance as a member of a QObject and move the object to that thread.

If you're going to use QThread, then I suggest reading and following the method as outlined in How to Really, Truly Use QThread.

In addition, it is often overlooked that the functionality of moveToThread allows a 1 to many relationship of QThread to QObject, so it is perfectly acceptable to create a QThread object and move multiple QObject instances to the new thread. Also, there is usually little benefit in creating more threads than available processor cores.

0
Zaiborg On

have a look at: what is the correct way to implement a QThread... (example please...)

and in addition to my comment, i do not think it is save to allocate an object on the stack and move it to a different thread.

0
S. Broekman On

For anyone interested, a little helper class that allows creating workers with automated variadic argument deduction. Note that C++17 is required (i.e. by which specification of the template arguments aren't necessary). For the full source code see: https://github.com/Broekman/Qt5_template

worker.hpp

#ifndef QT5_UI_WORKER_HPP
#define QT5_UI_WORKER_HPP

#include <QObject>
#include <QString>
#include <functional>
#include <tuple>

namespace ui
{
    class worker_object :
            public QObject
    {
    Q_OBJECT

    public:
        inline worker_object() = default;
        inline ~worker_object() = default;

    public slots:
        inline virtual void run() { /*...*/ };

    signals:
        void finished();
        void error(const QString& err_msg);
    };

    namespace helper
    {
        template <int... Is>
        struct index {};

        template <int N, int... Is>
        struct gen_seq : gen_seq<N - 1, N - 1, Is...> {};

        template <int... Is>
        struct gen_seq<0, Is...> : index<Is...> {};
    }

    template<typename... Ts>
    class worker :
            public worker_object
    {
    public: /* Functions */
        template<typename Func, typename... Args>
        inline worker(Func&& fn, Args&& ... args) :
                fn_(std::forward<Func>(fn)),
                args_(std::forward<Args>(args)...)
        { /*...*/ }

        inline ~worker() = default;

        inline void run() override
        {
            func(args_);
            emit finished();
        }

    private: /* Functions */
        template<typename... Args, int... Is>
        inline void func(std::tuple<Args...>& tup, helper::index<Is...>)
        {
            fn_(std::get<Is>(tup)...);
        }

        template<typename... Args>
        inline void func(std::tuple<Args...>& tup)
        {
            func(tup, helper::gen_seq<static_cast<int>(sizeof...(Args))>{});
        }

    private: /* Class members */
        std::function<void(Ts...)> fn_;
        std::tuple<Ts...> args_;
    };

    /**
     * @brief Helper function to create a worker by which specification of the template arguments aren't necessary.
     */
    template<typename Func, typename... Args>
    worker<Args...> make_worker(Func&& fn, Args&& ... args)
    {
        return worker<Args...>(std::forward<Func>(fn), std::forward<Args>(args)...);
    }
}

#endif

main_window.cpp

    void main_window::start_worker(worker_object *thread_worker, const worker_callback& on_finish,
                                   const worker_error_callback& on_error)
    {
        auto *worker_thread = new QThread;
        thread_worker->moveToThread(worker_thread);
        connect(thread_worker, &worker_object::error, this, on_error);
        connect(worker_thread, &QThread::started, thread_worker, &worker_object::run);
        connect(thread_worker, &worker_object::finished, worker_thread, &QThread::quit);
        connect(thread_worker, &worker_object::finished, this, on_finish);
        connect(thread_worker, &worker_object::finished, thread_worker, &worker_object::deleteLater);
        connect(worker_thread, &QThread::finished, worker_thread, &QThread::deleteLater);
        worker_thread->start();
    }

main_window.cpp example 1: no arguments

void main_window::service_example()
{
    //STEP 1: specify a (synchronous) task for the worker.
    auto *work = new worker(make_worker([this]()
    {
        auto task_len_ms = 2500; //ms
        logger_->info("Doing some concurrent work for " + std::to_string(task_len_ms) + " milliseconds...");
        QThread::msleep((unsigned)task_len_ms);
    }));

    //STEP 2: specify the completion handler. Called upon a worker_object::finished signal.
    auto on_finish_callback = [this]()
    {
        logger_->info("Concurrent work finished!");
    };

    //STEP 3: specify an error handler. Called upon a worker_object::error(const QString&) signal.
    auto on_error_callback = [this](const QString& err_msg)
    {
        logger_->error(err_msg.toStdString());
    };

    //STEP 4: start the worker.
    start_worker(work, on_finish_callback, on_error_callback);
}

main_window.cpp example 2: some arguments

//STEP 1: specify a (synchronous) task for the worker.
auto *work = new worker(make_worker([this](const std::string& personal_msg, unsigned long wait_time)
{
    logger_->info(personal_msg);
    QThread::msleep((unsigned)wait_time);
}, "Hello, world?", 2500));

//STEP 2: specify the completion handler. Called upon a worker_object::finished signal.
//STEP 3: specify an error handler. Called upon a worker_object::error(const QString&) signal.
//STEP 4: start the worker.