0
votes

Take the example from the Qt docs: one creates a controller and a worker, moves the worker to the specific thread and starts the thread. Start/stop triggering are done via signal/slot.

Now assume that the doWork method depends on some parameter (for example an output rate of intermediate results (in form of some visualization)) and one would like to tweak that parameter via a user interface while the function is running. If one uses the signal/slot mechanism (between interface and worker) this won't work, as the receiving slot of the worker will be queued after the running event loop and thus will be executed after doWork finished.

I came up with the following solutions:

Method 1:

Make the parameter public in Worker and change it directly via the interface.

class Worker : public QObject
{
    Q_Object

public:
    int parameter;
    ...
}

Interface::updateParameter(int p) { worker_instance.parameter = p; }

Method 2:

Move the parameter to either the interface itself or some extra class (living in another thread than the worker) and make the worker load it each time it's required.

class Interface : public QWidget
{
    Q_Object

private:
    int parameter;

public:
    int getParameter() { return parameter; }
    ...
}

Worker::doWork() {
    ...
    interface_instance.getParameter();  // load parameter;
    ...
}

Instead of placing the parameter within the interface one could place it in another class and interact with this class via signal/slot mechanism.

Regarding ease of use I'd definitely prefer Method 1. It also makes things stay where they actually belong to. Method 2 doesn't really seem to offer an advantage over Method 1. Any opinions on that?

Also are there any different approaches to handle things like that? For example viewing an animation, I guess one wants to enable adjustemt of the animation speed which somehow requires a hook into the animation process.

Do you have any ideas or knowledge on this topic?

3

3 Answers

1
votes

If one uses the signal/slot mechanism (between interface and worker) this won't work, as the receiving slot of the worker will be queued after the running event loop and thus will be executed after doWork finished.

This is not the necessarily case, you can make doWork() non-blocking, you do a portion of the work, then let the event loop cycle, allowing communication with other threads, you can use queued connections to transfer data this way, then do another work step, another cycle and so on. This allows you to monitor progress, pause, cancel work and transmit data back and forth during.

Blocking vs non-blocking worker:

main            main    
|       worker  |       worker
|------>|       |------>|
|       |       |       |
|       |       |<----->
|       |       |       |
|       |       |       |
|<------|       |<----->
|               |       |
|               |<------|
|               |   

Here I have set an example of breaking up work into steps and running the steps without blocking the worker thread to allow communications to get through. The concept is the same - split work into work units, track the state in the worker object, use signals and slots with parameters for control and data transfer.

Just take care not to go overboard with queued connections, because they are very slow compared to direct connections, if you flood the event loop you will actually lose performance and the application is likely to hang. Considering the goal is usually visual or user control feedback, try not to issue too many queued connections per second. Something like 30 per second is a sweet spot for responsive UI, but you could even do with less.

1
votes

It sounds a bit like you want thread synchronisation here, i.e. mutex and thread lock/unlock.

So in your code you would declare a mutex variable (if you have many workers and your parameter is worker specific then it should live publicly in the worker class, otherwise if its a "singleton" parameter then stick it in main). This is effectively a global variable. You also need a global variable that is your parameter.

Then in your doWork() function:

Worker::doWork() {
    ...
    // Get the parameter value
    g_mutex.lock();
    // take copy of the global parameter (m_ is member and g_ is global)
    m_param = g_param;  
    g_mutex.unlock();
    ...
}

Then wherever you need to acutally set/change the parameter:

    // Set the parameter value
    g_mutex.lock();
    g_param = ...;  
    g_mutex.unlock();

This ensures that the parameter is not overwritten / modified as you are trying to read it (if you are bothered). It's good thread-safe practise to do this. But its more important on the "set" side to use the mutex.

You can get away with no mutex on reads since you may not really care about someone writing to it at the same time (if its a single instruction write like a POD), but if its a structure then you probably will care.

Another way is to re-work your code so that your work units are smaller (I think like what @ddriver is getting at ... seems pretty good idea.

0
votes

This depends on the nature of your doWork. Is it expected to consume 100% of CPU, or is it event-based, triggered by something like a timed event? You mentioned visualisation, which means it is more like a timed event, where you wake up your thread, process/update the data, and then sleep until it is time to render the next frame.

If this is the case, you can indeed use the signal-slot to update data. To do this, you do not implement run(), thus using the default implementation (which uses its own event loop). In start() function then you queue a QTimer event to invoke your doWork(), and call the parent start(). The event loop starts, invokes your doWork(), you process your data (at your own pace - this is not GUI thread, you're not blocking anything), and at the end of doWork you queue another QTimer event again, and return to the event loop.

Since it is running the event loop, all signal/slot processing will work in this case. Even more, the queued connection signal/slot would be synchronous by definition, meaning you do not need to synchronize the data access.