GUI 在 Qt C++ 多线程中使用 Lambda 函数时会冻结。

huangapple go评论113阅读模式
英文:

GUI Freezes in Qt C++ Multithreading with Lambda function

问题

我有一个带有进度条的GUI,当我在GUI上单击运行时,它会进行大量的计算。进度条会更新以显示进度。当我让主线程执行所有计算时,这个工作得非常完美。但是这很慢,所以我更新为使用多个线程执行计算,每个线程都会相应地更新进度条。以下是我的实现(简化版):

main.cpp:

int main(int argc, char *argv[])
{
	QApplication a(argc, argv);
	// 做一些操作
	
	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)
{
	// 做一些操作
	// 设置GUI
}

void MainWindow::MyMainFunction(/*一些参数*/)
{
	// 做一些操作
	int progress;
	// 做更多操作并更新进度条
	ui->progressBar->setValue(progress);
	QApplication::processEvents();
	// 做更多操作并更新进度条
	ui->progressBar->setValue(progress);
	QApplication::processEvents();
	
	std::vector<double> myVector;  // 创建myVector并添加数据
	
	// 启动大量计算 - 使用多个线程
	int numThreads = QThread::idealThreadCount() * 0.75;  // 设置线程数
	int chunkSize = myVector.size() / numThreads;  // 设置每个线程要处理的数据块大小
	QVector<QThread *> threads(numThreads);  // 设置线程数组
	
	for (int threadNo = 0; threadNo < numThreads; threadNo = threadNo + 1)  // 遍历每个线程
	{
		int startIndex = threadNo * chunkSize;  // 获取线程的起始索引
		int endIndex = (threadNo + 1) * (chunkSize - 1));  // 获取线程的结束索引
		if (threadNo == numThreads - 1)  // 处理最后一个数据块
		{
			endIndex = myVector.size() - 1;  // 处理最后一个数据块
		}
		
		threads[threadNo] = new QThread;  // 初始化线程
		
		// 设置Lambda函数
		QObject::connect(threads[threadNo], &QThread::started, [&, &F1, &F2]
        {
            qDebug() << "在线程 " << QThread::currentThreadId() << " 中进行计算。";

            for (int i = 0; i < 60000; i = i + 1)  // 设置外部循环
			{
				int progress = // 快速计算以检查进度
				if (progress > ui->progressBar->value() + 1)  // 仅在进度增加至少1%时更新进度
				{
					// 在这里应用互斥锁,确保进度条不会被多个线程同时更新
					ui->progressBar->setValue(progress);  // 更新进度条
					QApplication::processEvents();  // 处理GUI事件
					// 在这里结束互斥锁,确保进度条不会被多个线程同时更新
				}
				
				for (int j = 0; j < 1000; j = j + 1)  // 设置内部循环
				{
					// 在这里进行大量计算
				}
			}
			
			return;  // 线程完成
		});
	}
	
	// 启动每个线程的工作
    for (int i = 0; i < numThreads; i = i + 1)
    {
        threads[i]->start();
        qDebug() << "线程 " << i << " 启动。";
    }

    // 等待每个线程完成
    QMutexLocker locker(&waitConditionMutex);
    for (int i = 0; i < numThreads; i = i + 1)
    { 
        waitCondition.wait(&waitConditionMutex);
        qDebug() << "线程 " << i << " 完成";
    }
    locker.unlock();

    // 现在删除线程
    for (int i = 0; i < numThreads; i = i + 1)
    {
        threads[i]->exit();
        threads[i]->wait();
        delete threads[i];
    }

    qDebug() << "所有线程已完成。";
}

现在的问题是,当计算开始时,进度条会在几秒钟内更新,然后GUI会冻结,但计算会在后台继续进行。线程正确地执行计算,显然速度更快,当计算完成时,GUI解冻。我不知道为什么GUI会冻结,不知道哪里出错。

我已经尝试调用一个函数来更新进度条,而不是直接使用ui->progressBar->setValue。我还尝试为numThreads添加0.75的因子,但也没有起作用。

我明白这是很多信息,但任何指导都将非常有用。我希望有一个简单的解决方案,或者我在某个地方出错了。

谢谢!

英文:

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-&gt;progressBar-&gt;setValue(progress);
QApplication::processEvents();
//do more stuff and update progress bar
ui-&gt;progressBar-&gt;setValue(progress);
QApplication::processEvents();
std::vector&lt;double&gt; 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&lt;QThread *&gt; threads(numThreads);					//set up the threads
for (int threadNo = 0; threadNo &lt; 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], &amp;QThread::started, [=,&amp;F1, &amp;F2]
{
qDebug() &lt;&lt; &quot;Processing calculations in thread &quot; &lt;&lt; QThread::currentThreadId() &lt;&lt; &quot;.&quot;;
for (int i = 0; i &lt; 60000; i = i + 1)				//set up outer loop
{
int progress = //quick calc to check the progress
if (progress &gt; ui-&gt;progressBar-&gt;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-&gt;progressBar-&gt;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 &lt; 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 &lt; numThreads; i = i + 1)
{
threads[i]-&gt;start();
qDebug() &lt;&lt; &quot;Thread &quot; &lt;&lt; i &lt;&lt; &quot; started.&quot;;
}
// wait for each thread to finish
QMutexLocker locker(&amp;waitConditionMutex);
for (int i = 0; i &lt; numThreads; i = i + 1)
{ 
waitCondition.wait(&amp;waitConditionMutex);
qDebug() &lt;&lt; &quot;Thread &quot; &lt;&lt; i &lt;&lt; &quot; finished&quot;;
}
locker.unlock();
// now delete the threads
for (int i = 0; i &lt; numThreads; i = i + 1)
{
threads[i]-&gt;exit();
threads[i]-&gt;wait();
delete threads[i];
}
qDebug() &lt;&lt; &quot;All threads finished.&quot;;
}

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!

答案1

得分: 4

问题是,为了及时更新GUI,Qt需要从主/GUI线程调用的任何函数快速返回。您的函数MyMainFunction()没有快速返回,因为它调用waitCondition.wait(&amp;waitConditionMutex)threads[i]-&gt;wait()来等待每个子线程,这些循环需要很长时间才能完成(即直到所有子线程执行完毕)。因此,MyMainFunction()直到所有子线程执行完毕才会返回。

为了避免这个问题,您需要重写MyMainFunction(),使其尽快返回,即使子线程仍在运行。(waitConditionMutex部分对我来说似乎是多余的;如果它与QThread::wait()调用重复,可以完全删除它)

这意味着您可能需要将QVector&lt;QThread *&gt; myVector变成MainWindow类的成员变量(这样您可以在MyMainFunction()返回后继续跟踪各个线程)。

您还需要一些方法,让子线程在希望主/GUI线程执行某些操作时向其发送信号。请注意,不允许子线程直接调用Qt GUI对象的方法,因此您在子线程的回调lambda内部调用ui-&gt;progressBar-&gt;setValue(progress);QApplication::processEvents()的调用是不允许的(它不会可靠工作)。相反,您可以创建一个排队信号/槽连接,允许您的子线程发出一个信号,随后将在主/GUI线程中调用相应的槽方法。或者,您的子线程可以调用qApp->postEvent()向主线程发送事件,然后您的代码可以通过事件回调方法处理它。

子线程可以在希望更新QProgressBar时向主线程发出信号,以及在处理完成并希望通知主线程调用QThread::wait()来清理其QThread对象时再次发出信号。(由于主线程将知道它们已经退出或即将退出,当它收到该消息时,它就不必担心QThread::wait()花费很长时间才返回)

英文:

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(&amp;waitConditionMutex) and threads[i]-&gt;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&lt;QThread *&gt; 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-&gt;progressBar-&gt;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)

答案2

得分: 0

感谢Jeremy Friesner的回答,我已经能够找到一个解决方案。

不幸的是,MyMainFunction()非常长,函数后续的工作需要使用lambda函数的结果。我目前无法重新格式化整个代码以使函数返回结果。

我找到了另一种解决方案:

  1. 我创建了一个变量(int trackProgress - 代表百分比),lambda函数可以访问它(使用&amp;trackProgress),每个子线程都会更新它(通过互斥锁,以防止未定义行为)。

  2. 而不是主线程坐在那里等待,我在lambda函数后面创建了一个循环,随着trackProgress变量的更新来更新GUI中的进度条。

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

这样,我创建了一个自己的循环,它不会退出,直到线程完成。这还意味着每个线程都安全地更新trackProgress,只有主线程更新GUI进度条。

希望这种方法没有关键缺陷,但它目前正常运行,GUI会更新进度条而不会冻结。

英文:

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 &amp;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 &lt;100)
{
ui-&gt;progressBar-&gt;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.

huangapple
  • 本文由 发表于 2023年4月4日 09:32:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/75924859.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定