kostrykin
kostrykin

Reputation: 4292

QThread: Making parallel execution with GUI-Feedback blocking?

Scenario

Lets say, I have a procedure called parallelRun. It would take a list of workers, each having a getWorkAmount():int, a run() method, a finished() signal and a cancel() slot:

void parallelRun( std::vector< Worker* > workers );

Its implementation should:

1. Open a QPogressDialog:

unsigned int totalWorkAmount = 0;
for( auto it = workers.begin(); it != workers.end(); ++it )
{
    totalWorkAmount += ( **it ).getWorkAmount();
}

LoadUI ui( 0, totalWorkAmount, this );

with

class LoadUI : public QObject
{
    Q_OBJECT

public:

    LoadUI( int min, int max, QWidget* modalParent )
        : totalProgres( 0 )
        , progressDlg( "Working", "Abort", min, max, modalParent )
    {
        connect( &progressDlg, SIGNAL( canceled() ), this, SLOT( cancel() ) );

        progressDlg.setWindowModality( Qt::WindowModal );
        progressDlg.show();
    }

    bool wasCanceled() const
    {
        return progressDlg.wasCanceled();
    }

public slots:

    void progress( int amount )
    {
        totalProgres += amount;

        progressDlg.setValue( totalProgres );
        progressDlg.update();

        QApplication::processEvents();
    }

signals:

    void canceled();

private slots:

    void cancel()
    {
        emit canceled();
    }

private:

    int totalProgres;
    QProgressDialog progressDlg;
}

2. Create one thread for each worker

std::vector< std::unique_ptr< QThread > > threads;
for( auto it = workers.begin(); it != workers.end(); ++it )
{
    std::unique_ptr< QThread > thread( new QThread() );

    Worker* const worker = *it;
    worker->moveToThread( thread.get() );

    QObject::connect( worker, SIGNAL( finished() ), thread.get(), SLOT( quit() ) );
    QObject::connect( &ui, SIGNAL( canceled() ), worker, SLOT( cancel() ) );
    QObject::connect( *it, SIGNAL( progressed( int ) ), &ui, SLOT( progress( int ) ) );

    thread->start( priority );

    threads.push_back( std::move( thread ) );
}

3. Run them simultaneously

for( auto it = workers.begin(); it != workers.end(); ++it )
{
    QMetaObject::invokeMethod( *it, "run", Qt::QueuedConnection );
}

load() is run when the user clicks an UI-button.

Problem

How am I supposed to extend this code, if I want to make parallelRun block until all workers are finished, without freezing the QProgressDialog?

Deliberations

Using a barrier

I tried adding the following code at the end of the parallelRun routine:

QApplication::processEvents();
for( auto it = threads.begin(); it != threads.end(); ++it )
{
    ( **it ).wait();
}

The impact of this few lines of extra-code is, that LoadUI::progress is never entered, since the GUI-thread is asleep and therefore it's event loop isn't processed: In Qt, signals are delivered to slots by posting them to the event loop of the thread, associated to the object the slot belongs to. This is why the progressed signal of a worker is never delivered.

I think, the appropriate solution would be to run QApplication::processEvents() within the GUI-thread anytime a progressed signal is emitted by a worker. On the other hand, I guess this cannot be done, since the GUI-thread is asleep.

Another possible solution

Another possibility would be to use an active waiting-like solution:

for( auto it = threads.begin(); it != threads.end(); ++it )
{
    while( ( **it ).isRunning() )
    {
        QApplication::processEvents();
    }
}
for( auto it = threads.begin(); it != threads.end(); ++it )
{
    ( **it ).wait();
}

This also requires adding the following line of code right after thread->start( priority );:

while( !thread->isRunning() );

I don't think that this is a nice solution, but at least it works. How can this be done without the drawbacks of active waiting?

Thanks in advance!

Upvotes: 3

Views: 2987

Answers (2)

alexisdm
alexisdm

Reputation: 29886

You could use the threads' finished() signals to wait for them all to finish in the main GUI loop instead of using QApplication::processEvents. The progress dialog modality will ensure that only that dialog window is active until it is explicitly closed.

class WorkerManager : public QObject {
    Q_OBJECT
private:
    // to be able to access the threads and ui, they are defined as a members
    std::vector<std::unique_ptr<QThread> > threads;
    LoadUI *ui;

    int finishedThreadCount;
public:
    WorkerManager() 
        : finishedThreadCount(0)
    {
        // Open the QProgressDialog
        ...
        // Create and start the threads
        ...
        // Connect the finished() signal of each thread 
        // to the slot onThreadFinished
        for( auto it = threads.begin(); it != threads.end(); ++it )  {
            QObject::connect(
                it->get(), SIGNAL(finished()), 
                this, SLOT(onThreadFinished()) );
        }
    }

private slots:
    void onThreadFinished() {
         ++finishedThreadCount;

         if(finishedThreadCount == threads.size()) 
         {
              // clean up the threads if necessary
              // close the dialog
              // and eventually destroy the object this itself
         }
    }
};

Or you can run a nested QEventLoop to wait for the threads to finish synchronously while still keeping the GUI responsive:

// Open the QProgressDialog
...
// Create and start the threads
...
// Create and run a local event loop,
// which will be interrupted each time a thread finishes
QEventLoop loop;
for( auto it = threads.begin(); it != threads.end(); ++it )  
{
    QObject::connect(
        it->get(), SIGNAL(finished()), 
        &loop, SLOT(quit()) );
}  
for(int i = 0, threadCount = threads.size(); i < threadCount; ++i) 
    loop.exec();

If the progress reach the maximum only when the work is completely done, you can use progressDlg->exec() instead of a QEventLoop which will block until the maximum is reached or until the user clicks on the "Cancel" button.

Upvotes: 1

Tobias Hieta
Tobias Hieta

Reputation: 189

Instead of building your own. Maybe QThreadPool is what you are looking for?

QThreadPool has a function for waiting for all workers.

Upvotes: 1

Related Questions