0

I have a GUI with a progress bar, and when I click RUN on my GUI, it progress a lot of calculations. The progress bar is updated to show progress. This works perfectly fine when I had the main thread do all of the calculations. This was very slow so I have updated to have multiple threads do the calculations, each updating the progress bar accordingly as they go. Here is my implementation (simplified):

main.cpp:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    //do some stuff
    
    MainWindow w(F1, F2);
    w.show();
    return a.exec();
}

mainwindow.cpp

MainWindow::MainWindow(int F1, int F2,  QWidget *parent):

    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    //do some stuff
    //set up GUI
}

void MainWindow::MyMainFunction(/*some parameters here*/)
{
    //do some stuff
    int progress;
    //do more stuff and update progress bar
    ui->progressBar->setValue(progress);
    QApplication::processEvents();
    //do more stuff and update progress bar
    ui->progressBar->setValue(progress);
    QApplication::processEvents();
    
    std::vector<double> myVector;   //create myVector and add data
    
    //start a LOT of calculations - using multiple threads
    int numThreads = QThread::idealThreadCount() * 0.75;    //set the number of threads
    int chunkSize = myVector.size() / numThreads;           //set the chunk size for each thread to process
    QVector<QThread *> threads(numThreads);                 //set up the threads
    
    for (int threadNo = 0; threadNo < numThreads; threadNo = threadNo + 1)  //loop through each thread
    {
        int startIndex = threadNo * chunkSize;              //get the start index for the thread
        int endIndex = (threadNo + 1) * (chunkSize - 1));   //get the end index for the thread
        if (threadNo == numThreads - 1)                     //deal with the last chunk
        {
            endIndex = myVector.size() - 1;                 //deal with the last chunk
        }
        
        threads[threadNo] = new QThread;                    //initiate thread
        
        //set up lambda function
        QObject::connect(threads[threadNo], &QThread::started, [=,&F1, &F2]
        {
            qDebug() << "Processing calculations in thread " << QThread::currentThreadId() << ".";

            for (int i = 0; i < 60000; i = i + 1)               //set up outer loop
            {
                int progress = //quick calc to check the progress
                if (progress > ui->progressBar->value() + 1)    //only update the progress if it has increased by at least 1%
                {
                    //APPLY MUTEX LOCK HERE                     //make sure progress bar isnt updated by more than one thread at once
                    ui->progressBar->setValue(progress);        //Update progress bar
                    QApplication::processEvents();              //process GUI events
                    //END MUTEX LOCK HERE                       //make sure progress bar isnt updated by more than one thread at once
                }
                
                for (int j = 0; j < 1000; j = j + 1)            //set up inner loop
                {
                    
                    //DO LOADS OF CALCULATIONS HERE             //do calculations
                    
                }
            }
            
            return;                                             //thread finished
        });
    }
    
    // Start the work for each thread
    for (int i = 0; i < numThreads; i = i + 1)
    {
        threads[i]->start();
        qDebug() << "Thread " << i << " started.";
    }

    // wait for each thread to finish
    QMutexLocker locker(&waitConditionMutex);
    for (int i = 0; i < numThreads; i = i + 1)
    { 
        waitCondition.wait(&waitConditionMutex);
        qDebug() << "Thread " << i << " finished";
    }
    locker.unlock();

    // now delete the threads
    for (int i = 0; i < numThreads; i = i + 1)
    {
        threads[i]->exit();
        threads[i]->wait();
        delete threads[i];
    }

    qDebug() << "All threads finished.";
}

The problem now is that when the calculations start, the progress bar updates for a couple of seconds and then the GUI freezes, but the calculations carry on in the background. The threads are doing the calculations correctly, obviously its a lot faster now, and when the calculations are done, the GUI unfreezes. I dont know what I am doing wrong to make the GUI freeze.

I have tried to call a function to update the progress bar, rather than directly using ui->progressBar->setValue. I have also tried to add in a 0.75 factor to the numThreads, but that also didnt work.

I appreciate this is a lot of information, but any guidance would be useful. I'm hoping there is an easy solution, or I have messed up somewhere.

Thank you!

2 Answers2

4

The problem is that in order to update the GUI in a timely manner, Qt needs any functions called from the main/GUI thread to return quickly. Your function MyMainFunction() is not returning quickly, because it calls waitCondition.wait(&waitConditionMutex) and threads[i]->wait() for each child thread, and these loops won't complete for a long time (i.e. not until all child threads have finished executing). Therefore MyMainFunction() won't return until all the child threads have finished executing.

To avoid that problem, you need to rewrite MyMainFunction() so that it returns ASAP, i.e. while the child threads are still running. (The waitConditionMutex stuff looks unnecessary to me; if it's redundant with the QThread::wait() calls, it can be removed entirely)

That means that you'll probably want to make your QVector<QThread *> myVector into a member-variable of the MainWindow class (so that you can keep track of the various threads even after MyMainFunction() has returned).

You'll also need some way for your child threads to send signals to the main/GUI thread when they want it to do something. Note that child threads are not allowed to call methods on Qt GUI object directly, so your call to ui->progressBar->setValue(progress); or QApplication::processEvents() from within the child thread's callback-lambda is a no-no (it will not work reliably). Instead, you can create a Queued signal/slot connection that allows your child thread to emit a signal which will cause the corresponding slot-method to be called shortly thereafter in the main/GUI thread. Or alternatively you child could call qApp->postEvent() to post an event to the main thread, which you code could then handle via an event-callback method.

Child threads could signal the main thread whenever they want it to update the QProgressBar, and again when they are done processing and want to notify the main thread to call QThread::wait() on their QThread object to clean up. (Since the main thread will know they've already exited -- or are about to exit -- when it gets that message, it doesn't have to worry about QThread::wait() taking a long time before returning)

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
0

Thanks to Jeremy Friesner answer, I have been able to find a solution.

Unfortunately the MyMainFunction() is really long and subsequent work done by the function requires the result of the lambda function. I am not able to reformat my whole code at this stage to get the function to return.

I have found an alternative solution:

  1. I created a variable (int trackProgress - represents a percentage) that the lambda function has access to (using &trackProgress), which each child thread updates (via a mutex, to prevent undefined behaviour) as it progresses.

  2. Rather that the main thread then just sitting and waiting, I created a loop after the lambda function which updates the progress bar in the GUI as the trackProgress variable is updated

    while(trackProgress <100)
    {
        ui->progressBar->setValue(trackProgress);
        std::chrono::seconds dura(1);
        std::this_thread::sleep_for(dura);
    }

This way, I have created my own loop that wont exit until the threads finish. It also means that trackProgress is safely updated by each thread, and that only the main thread updates the GUI progress bar.

Hopefully there aren't any critical flaws with this approach, but it is currently working fine and the GUI updates the progress bar without freezing.