从多个 QThread 更新 QProgressBar

Posted

技术标签:

【中文标题】从多个 QThread 更新 QProgressBar【英文标题】:Update QProgressBar from multiple QThreads 【发布时间】:2014-06-11 15:04:30 【问题描述】:

我在网上找到了几个教程,解释了如何在长时间计算期间更新 QProgressBar。其中之一是:使用 QThread 进行计算,然后发出连接到progressBar.setValue(int) 的信号。

我认为这也必须适用于同时运行的多个 QThread,但有些东西无法正常工作。

所以,这就是我要做的:我想计算几个粒子的轨迹(每个粒子都有一个长循环)。为了使用多核处理,我为这些粒子中的每一个创建了一个 QThread,并让它调用各自的计算方法。这工作正常,所有内核都已使用,并且计算完成时间比以前大约缩短了四分之一。

我根据本教程 http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/ 编写了一个 Worker 类。标题如下所示: (工人.h)

#include <world.h>

class Worker: public QObject

    Q_OBJECT

public:
    explicit Worker(World *world = 0, double deltaTau = 0., double maxDist = 0., double iterations = 0., double index = 0);

public slots:
    void process();

signals:
    void finished();
    void eror(QString err);

private:
    World *w;
    double my_deltaTau;
    double my_maxDist;
    int my_iterations;
    int my_index;
;

来源如下: (worker.cpp)

#include "worker.h"

Worker::Worker(World *world, double deltaTau, double maxDist, double iterations, double index)

    w = world;
    my_deltaTau = deltaTau;
    my_maxDist = maxDist;
    my_iterations = iterations;
    my_index = index;


void Worker::process()

    w->runParticle(my_deltaTau, my_maxDist, my_iterations, my_index);
    emit finished();

在 world.cpp 中,我有一个启动所有线程的函数 run 和一个由 Worker 调用的函数 runParticle

void World::run(double deltaTau, double maxDist, int iterations)

    globalProgress = 0;
    for (int j = 0; j < particles->size(); j++)  //loop over all particles
        QThread *thread = new QThread;
        Worker *worker = new Worker(this, deltaTau, maxDist, iterations, j);
        worker->moveToThread(thread);
        connect(thread, SIGNAL(started()), worker, SLOT(process()));
        connect(worker, SIGNAL(finished()), thread, SLOT(quit()));
        connect(worker, SIGNAL(finished()), thread, SLOT(deleteLater()));
        connect(thread, SIGNAL(finished()), worker, SLOT(deleteLater()));
        thread->start();
    


void World::runParticle(double deltaTau, double maxDist, int iterations, int index)

    for (int i = 0; i < iterations; i++)  //loop over iteration steps
        if (i % 1000 == 0)  //only update the progress bar every 1000th iteration
            emit updateProgress(++globalProgress);
            qApp->processEvents(); // <--- I added this line, no effect!
        
        [...] // <--- do my calculations for the particle's trajectories
    

公共槽 updateProgress(int) 在此处每 1000 次迭代步骤调用一次。它像这样连接到我的 MainWindow 中的 QProgressBar:

progressBar->setValue(0);
progressBar->setMaximum(nrPart * iter / 1000); //number of particles * number of iteration steps / 1000
connect(world, SIGNAL(updateProgress(int)), progressBar, SLOT(setValue(int)));
world->run(timeStep, dist, iter);

我的问题是进度条在所有计算完成之前不会移动,然后我看到它很快就会移动到 100%。

有没有人看到我的错误或知道如何正确地做到这一点?

编辑

我做了以下更改:

(worker.h)

#include "world.h"

class Worker: public QObject

    Q_OBJECT

public:
    explicit Worker(World *world = 0, Particle *particle = 0, QList<MagneticField> *bfields = 0, double deltaTau = 0., double maxDist = 0., int iterations = 0);

public slots:
    void process();

signals:
    void finished();
    void updateProgress(int value);
    void ProcessParticle();
    void eror(QString err);

private:
    int i;
    Particle *p;
    QList<MagneticField> *magneticFields;
    double my_deltaTau;
    double my_maxDist;
    int my_iterations;
;

(worker.cpp)

#include "worker.h"

Worker::Worker(World *world, Particle *particle, QList<MagneticField> *bfields, double deltaTau, double maxDist, int iterations)

    i = 0;
    const World *w = world;
    p = particle;
    magneticFields = bfields;
    my_deltaTau = deltaTau;
    my_maxDist = maxDist;
    my_iterations = iterations;
    connect(this, SIGNAL(updateProgress(int)), w, SLOT(updateTheProgress(int)));
    connect(this, SIGNAL(ProcessParticle()), this, SLOT(process()), Qt::QueuedConnection);


void Worker::process()

    const int modNr = my_iterations / 1000;
    QDateTime start = QDateTime::currentDateTime();
    while (i < my_iterations)  //loop over iteration steps
        [...] // <--- do my calculations
        //handle progress
        emit updateProgress(1);
        if (QDateTime::currentDateTime() > start.addMSecs(300)) 
            emit ProcessParticle();
            ++i; //ensure we return to the next iteration
            return;
        
        i++;
    
    qDebug() << "FINISHED"; // <--- I can see this, so finished() should be emitted now...
    emit finished();

(世界.h的一部分)

public slots:
    void threadFinished();
    void updateTheProgress(int value);

signals:
    void updateProgress(int value);

(world.cpp 的一部分)

void World::threadFinished()

    particleCounter++;
    qDebug() << "particles finished: " << particleCounter; // <--- this is NEVER called !?!?
    if (particleCounter == particles->size()) 
        hasRun = true;
    


void World::updateTheProgress(int value)

    globalProgress += value;
    emit updateProgress(globalProgress);


void World::run(double deltaTau, double maxDist, int iterations)

    globalProgress = 0;
    particleCounter = 0;
    hasRun = false;
    for (int i = 0; i < particles->size(); i++)  //loop over all particles
        QThread *thread = new QThread;
        Worker *worker = new Worker(this, &(*particles)[i], bfields, deltaTau, maxDist, iterations);
        worker->moveToThread(thread);
        connect(thread, SIGNAL(started()), worker, SLOT(process()));
        connect(worker, SIGNAL(finished()), thread, SLOT(quit()));
        connect(worker, SIGNAL(finished()), thread, SLOT(deleteLater()));
        connect(worker, SIGNAL(finished()), this, SLOT(threadFinished())); // <--- this connection SHOULD make sure, I count the finished threads
        connect(thread, SIGNAL(finished()), worker, SLOT(deleteLater()));
        thread->start();
    

(在 MainWindow.cpp 中的某处)

progressBar->setValue(0);
progressBar->setMaximum(nrPart * iter);
connect(world, SIGNAL(updateProgress(int)), progressBar, SLOT(setValue(int)));
world->run(timeStep, dist, iter);
while (!world->hasBeenRunning())  //wait for all threads to finish

正如我在上面的代码中所标记的,当线程完成时我永远不会收到通知,并且我最终会在 MainWindow 中陷入无限循环。 World Worker 连接还有问题吗?

【问题讨论】:

位于单独线程中的Worker 调用位于主线程中并尝试发出信号的World 上的方法这一事实让我感到紧张。 updateProgress 信号确实应该是 worker 上的信号,以便 connect 调用将其识别为跨线程信号。不确定这是否是问题所在,但根据我对 Qt 线程模型的了解,您的代码不符合犹太教规。 您的 WorldWorker 类似乎非常紧密耦合。考虑重新设计您的 World 类,使其对您的 Worker 类和迭代一无所知。将迭代和信号发射移动到Worker 类,然后将其连接到您的进度条。 【参考方案1】:

IMO 这是错误的。创建线程的成本很高,并且您想要创建很多线程。 首先你应该使用QThreadPool,你的case完全匹配这个类的功能。

还有QtConcurrent 方法可以大大减少样板代码(这是Qt 的废弃特性,因此建议使用QThreadPool,但您可以尝试一下,效果很好)。

【讨论】:

以前从未听说过 QThreadPool,但它确实听起来像是我应该在这里使用的。到目前为止,我“只”模拟了 100 个粒子——即一次创建 100 个线程——它还没有崩溃。如果我尝试 1000 个粒子,它会立即崩溃。希望 QThreadPool 解决这个问题... @Bianfable,我刚刚注意到这一点。这里的问题不是 QThread 的使用,而是你如何使用它。您不需要比处理器内核更多的线程,并且不会从中受益。不要为每个粒子创建一个线程。相反,将多个粒子工作对象移动到一个线程。 好的,我现在基本上重写了所有代码以执行以下操作:从 MainWindow 我调用 world->run(...),然后使用 QtConcurrent::map(...) 启动我计算的 正确 线程数。然后我使用 QFutureWatcher 获取进度并将其显示在 QProgressDialog 中,如下所述:bogotobogo.com/Qt/…。它仍然以某种方式崩溃,但这不属于这里,因为进度条问题已解决!【参考方案2】:

问题在于,为了处理发出的信号,您需要允许事件循环在新线程上运行。通过保留在 runParticle 中的 for 循环中,这不会发生,直到函数完成。

有一种粗略的方法可以解决这个问题,即在循环期间每隔一段时间调用一次 QApplication::processEvents。

更好的方法是重新设计对象,以便在退出之前处理多次迭代并让事件循环自然运行。

因此,要为处理创建一个时间片,在您的 for 循环中,您需要计算迭代所用的时间。如果时间已超过 1/30 秒,则调用 QueuedConnection 类型的信号再次调用您的插槽函数并退出 for 循环。

QueuedConnection 将确保处理任何事件,然后再次调用您的函数。

假设 runParticle 是一个槽:-

void Worker::runParticle(...)

    static int i = 0;

    QDateTime start = QDateTime::currentDateTime();

    for(i; i < iteration; ++i)
    
        // do processing


        emit updateProgress(++globalProgress);

        // check if we've been here too long
        if(QDateTime::currentDateTime() > start.addMSecs(300))
        
          emit ProcessParticle(); // assuming this is connected to runParticle with a Queued Connection
          ++i; // ensure we return to the next iteration
          return;
        
    

另一方面,当一个对象被移动到一个新线程时,它的所有子对象也会被移动,其中一个子对象是 QObject 层次结构的一部分。

通过让 Worker 对象持有指向 World 的指针,它直接调用 World 的 runParticle 函数,该函数仍在主线程上。虽然这是不安全的,但这也意味着 runParticle 函数正在主线程上处理。

您需要将 runParticle 函数移动到新线程上的 Worker 对象。

【讨论】:

如您所见,我已经尝试使用 qApp->processEvents() 并且 QApplication::processEvents() 不会改变循环中的任何内容。您的其他建议听起来很有趣,但我不知道如何进行排队连接!? Qt::QueuedConnection 是您传递给调用连接的参数。见qt-project.org/doc/qt-4.8/qobject.html#connect-3 所以我让 runParticle 成为一个插槽并添加一个与该参数相关的新信号 ProcessParticle?我在哪里连接这些? 我已经编辑了我的答案。请阅读下面的示例代码。至于连接,你可以直接在 Worker 类的构造函数中进行:connect(this, &Worker::ProcessParticles, this, &Worker::runParticle, Qt::QueuedConnection); 啊,好的,但这有点烦人,因为计算需要从我在 World 中有一个 QList 的类 Particle(我没有显示)中访问很多东西。然后我会尝试将这些东西移到 Worker 中...

以上是关于从多个 QThread 更新 QProgressBar的主要内容,如果未能解决你的问题,请参考以下文章

PyQt5 QThread 不会因终止或标志变量而停止

PyQt5 - 使用QThread更新Qtablewidget

使用 QNetworkAccessManager 时启动 QThread 失败

Qt多线程操作界面---在QThread更新QProgressBar

PyQT和线程

在 PyQt4 中使用 QThread 运行线程时更新变量值