Multi-threading design along with an undo/redo stack

Viewed 243

I have this call stack to perform a heavy computation:

// QML

StyledButton {
    onButtonClicked: {
        registeredCppClass.undoHandler.createCommand()
    }
}
void UndoHandler::createCommand()
{
    m_undoStack->push(new Command());
}
class Command : public QUndoCommand
{
public:
    Command();
    virtual ~Command();

    virtual void undo();
    virtual void redo();
    
    // ...
private:
    // Handler does the logic
    LogicHandler *m_logicHandler;
    // Output by logic handler
    QString m_outputName;
};
void Command::redo()
{
    
    if (/* */) {
        
    } else {
        // Run heavy computation
        m_outputName = m_logicHandler->run();
    }
}
QString LogicHandler::run()
{
    // Heavy computation starts
}

Intention

I intend to implement QThread by this approach to prevent GUI from getting non-responsive while heavy computation is being done. But I don't know where QThread and Worker class need to be implemented. Should they be within:

  • UndoHandler::createCommand
  • Command::redo
  • LogicHandler::run
  • ... ?

What is the best location for QThread and Worker considering their signal-slot connections?

2 Answers

The general advice is to never read QThread documentation. Follow that up with never read Linux thread documentation. I say this as someone who has written quite a few books on software development.

The long answer is threading wasn't well thought out early on and there is a lot of, now bad, information out there. During Qt 3.x and I believe early Qt 4.x one was supposed to derive a class from QThread and override the run(). You can imagine just how well that worked for newbie developers unfamiliar with threads in general and unable to design in mutex or other access protection when manipulating things in multiple threads.

Your design makes it appear you have read some of this documentation. It's still floating around out there.

At some point during Qt 4.x we were no longer supposed to derive from QThread. Instead we were supposed to just create a QThread and moveToThread(). Kinda sorta worked but you could still end up with "dangling threads" if your program didn't follow the happy path through the code.

Around the same time, at least as far as my exposure, we also got a global thread pool.

Your design is really flawed because you looked at old doc. Not your fault. The old doc tends to turn up first in searches.

Visit this GitHub repo and pull down the project. The only dev_doc setup documentation I have completed is for Fedora. I will be working on Ubuntu this morning if I don't get interrupted. Be sure to check out the diamond-themes branch.

Yes, this is using CopperSpice, but CopperSpice is a fork of Qt 4.8 and this is the only concrete code example I could think of off the top of my head. You can build and run the editor or you can poke-and-hope by reading advfind_busy.cpp. What you are looking for is how QFuture is used. That source file is only about 200 lines long and it has a short header file.

Throw out your current design. You need QFuture and QtConcurrent::run().

Note: The header files for these things are different in name and location when compared to current Qt 5.x. That much you will need to look up if you choose to stay with Qt. How you use this stuff is not.

Note 2: If you don't have some kind of throttle control to limit each of these tasks to a single thread instance you will need to dynamically create and destroy QFuture objects. This means you have to have some kind of list or vector keeping track of them and your object destructor needs to walk that list killing off the threads and deleting the objects.

If you want to go on a journey setting up CopperSpice on Ubuntu it is spread across a multi-part blog post starting here.

IMHO, your intentions are correct, and you are headed in the right direction (leaving aside the argument for using QtConcurrency -- thread pools and futures -- since that's not pertinent to the immediate question). Let's address the first part: the objects and execution flow.

As the classes have been outlined in the code snippets, you will need to take extra care to correctly push them across thread boundaries. If you think for a moment, the worker object is created in the calling thread, therefore some of the object's members will also be created in the calling thread. For members which are pointers, this does not pose much of a problem, because you may elect to delay the creation of those objects until after the enclosing object instance has been created and moved to the worker thread. But, embedded objects are created when the object is constructed. If the embedded object derives from QObject, it will have its thread affinity set to the caller thread. In such a case, signals won't work properly. To alleviate this problem, it is often easiest to pass the work thread to the worker object's constructor, so the worker object is able to move all of its embedded objects to the worker thread.

Second, assuming the following:

  1. Command holds a unique instance of LogicHandler, and
  2. LogicHandler does not have state, and
  3. LogicHandler is a subclass of QObject, and
  4. LogicHandler is the worker class

My advice would be to place the spinning up of the thread in Command::redo, then connect the signals similar to advice given at the bottom of this article. Also, you would not set Command.m_outputName to the return value of LogicHandler::run. LogicHandler::run should return void. Instead, you should add a signal to LogicHandler that emits the string value when it has finished processing; then, add a slot in Command to handle that. A QString can easily be marshaled across thread boundaries (make sure you make the connections of the proper type, see here).

The connecting of the worker startup method, to the threads started signal gets the execution started. There is no need to inherit from QThread and override run. The worker should also emit a finished signal, that should be connected to the thread's quit slot. The worker's finished signal should also be connected to both the thread's and worker's deleteLater slot. When these are setup, just call the thread's start method.

From there, the execution will return from redo, and you will be notified that the worker is finished when it emits a signal (the one I mentioned that you will need to add) and passes the output string. If the lifetime of the worker is different (I'm guessing longer, since you need to spin up a thread to do long operation) from the instance of Command, then you will need to connected the return value signal from the worker object to a different object.

Related