在Qt中正确的线程使用方式

6

我有一个耗时较长的图像加载问题(图片很大),而且在加载时还执行一些操作。我不希望阻塞应用程序GUI。

我的想法是在另一个线程中加载图像,发出图像加载完成的信号,然后重新绘制带有这个图像的视图。

我的方法:

void Window::loadImage()
{ 
    ImageLoader* loaderThread = new ImageLoader();
    connect(loaderThread,SIGNAL(imageLoaded()),this,SLOT(imageLoadingFinished());
    loaderThread->loadImage(m_image, m_imagesContainer, m_path);
}
void Window::imageLoadingFinished()
{
    m_imagesContainer->addImage(m_image);
    redrawView();
}

class ImageLoader : public QThread
{
    Q_OBJECT
    public:
       ImageLoader(QObject *parent = 0) : m_image(NULL), m_container(NULL)

       void loadImage(Image* img, Container* cont, std::string path)
       {
            m_image = img;
            m_container = cont;
            ...
            start();
       }
    signals:
       void imageLoaded();
    protected:
       void run()
       {
           //loading image and operations on it
           emit imageLoaded();
       }
    protected:
       Image* m_image;
       Container* m_container;
}

我参考了Qt中的quedcustomtype示例来编写这段代码。当我在Google和StackOverflow上搜索时,我还发现继承QThread并不是一个好主意。

那么正确的做法是什么?正如我所说,我想要非阻塞GUI,在另一个线程中进行加载和操作,并发送一个信号,表示加载完成。在信号被发出后,视图应该被重绘。 我不太了解多线程,但我认为可以理解或具有足够的基本知识来理解基本思想。


如果 QThread 本质上是有问题的,它就会被从库中移除。与其仅仅相信“永远不要使用 QThread”,你应该找出人们认为存在的问题,决定它们是否对你有效,并决定如何利用它们。或者,你可以使用平台特定(不一定可移植)的线程机制,例如 pthreads。只需不要在线程中驱动 UI,而是使用 Qt 事件告诉 UI 线程来驱动它。 - mah
@mah 我不认为QThread本质上是不好的,但是有一些人对如何使用它有不同的理解 - Andreas Fester
@Andreas 那篇博客似乎表明很多人不知道如何正确使用QThread。文章底部的链接有一个例子页面,其要点似乎是:为了正确地子类化QThread,必须重载 run() 方法。我真的不确定为什么你的链接提出了一种误用的模式(有效或无效),因为Qt的文档似乎非常清晰。例如:http://doc.qt.digia.com/qt/threads-starting.html。 - mah
可能会对您感兴趣:https://dev59.com/V2855IYBdhLWcg3w5InZ - g19fanatic
2个回答

7
使用QtConcurrent框架。
#include <QtConcurentRun>
#include <QFutureWatcher>

//....
class Window: public QWidget /*or something*/
{
//....
private:
    QFutureWatcher<QImage> _wacther; //this object will signal when loading finished
};

//...

void Window::loadImage()
{
   connect(&_watcher, SIGNAL(finished(), SLOT(finishLoading());
    _wacther.setFuture(QtConcurent::run<QImage>(this, &Window::doLoadImage));
}

QImage Window::doLoadImage() //this function will be executed in the new thread. SHOULD BE Thread Safe
{
   return someImage;
}

void window::finishLoading()
{
    QImage result = _watcher.result();
}

1
好主意,你比我先想到了它。有一个小问题,全局作用域中的_watcher变量不应该以_开头,因为在全局作用域中这是保留的。请参见https://dev59.com/KHVC5IYBdhLWcg3woSxW - odinthenerd
@PorkyBrain,它可能应该是“Window”的成员。让我把它放在那里。 - Lol4t0
1
注意拼写错误。 - koan
我认为我简化得太多了。我不想把问题弄复杂,所以发布了一个与我实际情况略有不同的例子。问题出现在调用 QtConcurrent::run<MyStructure*>(*this,&ClassName::function) 上。输出说 QObject::QObject(const QObject&) is private。我已经尝试了 QtConcurent::run<MyStructure*>(&className::function),但也失败了。我看到某个地方禁止复制。如果这个评论里提供的信息不足,请告诉我需要澄清哪些地方。 - krzych
考虑到Qt团队并不认为QtConcurrent是最好的选择:http://www.mail-archive.com/development@qt-project.org/msg07794.html。我喜欢它,但我一直被劝退使用它。 - Luca Carlon
显示剩余3条评论

3

我认为这是最佳方案:

#include <QApplication>
#include <QLabel>
#include <QThread>

class ImageLoader : public QObject
{
   Q_OBJECT
public:
   ImageLoader() : QObject() {
      moveToThread(&t);
      t.start();
   }
   ~ImageLoader() {
      qDebug("Bye bye!");
      t.quit();
      t.wait();
   }

   void requestImage(QString absPath) {
      QMetaObject::invokeMethod(this, "loadImage", Q_ARG(QString, absPath));
   }

public slots:
   void loadImage(QString absPath) {
      // Simulate large image.
      QImage image(absPath);
      sleep(10);
      qDebug("Image loaded!");
      emit imageReady(image);
   }

signals:
   void imageReady(QImage image);

private:
   QThread t;
};

class MyLabel : public QLabel
{
   Q_OBJECT
public:
   MyLabel() : QLabel() {}

   void mousePressEvent(QMouseEvent* ev) {
      Q_UNUSED(ev);
      qDebug("I got the event!");
   }

public slots:
   void setImage(QImage image) {
      setPixmap(QPixmap::fromImage(image));
      resize(image.width(), image.height());
      qDebug("Image shown!");
   }
};

int main(int argc, char *argv[])
{
   QApplication a(argc, argv);

   MyLabel label;
   label.show();

   ImageLoader imageLoader;
   QObject::connect(&imageLoader, SIGNAL(imageReady(QImage)), &label, SLOT(setImage(QImage)));
   imageLoader.requestImage(some_abs_path);

   return a.exec();
}

#include "main.moc"

我也喜欢QtConcurrent,但需要考虑到它的使用有些被不鼓励:http://www.mail-archive.com/development@qt-project.org/msg07794.html


你将ImageLoader移动到线程中,但同时,你也将Thread移动到它的“自己”的线程中,因为它是一个成员变量。QThread只是线程管理器,不应该被移动。请看官方文档如何使用Controller(包装线程)和worker(使用moveToThread())。 - B. Decoster
不,根据文档,moveToThread移动的是子对象:http://doc.qt.io/qt-5/qobject.html#moveToThread。QThread并不是一个子对象,它只是一个成员。文档明确指出:http://doc.qt.io/qt-5/qobject.html#thread-affinity:“当调用moveToThread()时,对象的成员变量将保留在旧线程中”。这意味着QThread将保留在原始线程中,只有对象被移动了,因为没有子对象。 - Luca Carlon
你能快速编辑一下,这样我就可以撤销我的投票吗?似乎只需要添加一个空格就可以了。 - B. Decoster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接