手记

Essential Qt 第二十六章 多线程

      多线程是一个很大的范围,内容也非常的多,我手上就有三本总计超过1500页的书讲述多线程的问题,这一章只能演示下Qt对多线程提供的一些支持。多线程有很强的平台相关性,很多时候需要用到个平台的API,这对于移植提出了挑战,而Qt提供的线程相关的类可以在各个平台上使用,对于很多开发者来说,这可以节约很多时间和精力。

      本章只讨论Qt的线程的一些类及用法,这里假设你已经对线程有一定的了解,类似线程,互斥锁,互斥量,死锁,线程安全,原子操作等一些术语假设你已经了解他们的含义,这里不多做介绍了。

       在第二十一章里有个显示本地目录的演示程序,这个程序遇到一个问题,由于需要加载的目录数量过多,程序启动需要花费大量时间,为了在在Linux下运行,甚至取消了有大量文件的两个目录,当然对于一个用于演示的程序来说,这到没有什么太大关系,但这里的真正问题在于:当程序直线某个比较费时的操作时,界面就会卡死。

      


      这里有个很简单的演示程序,这个程序有个选择本地目录的功能,当用户选择了某个目录的话,这个程序就可以显示该目录下所有图片的缩略图,但假如一个目录下的图片比较多,单幅图片比较大时,界面就会处于卡死状态,卡死的时间取决于加载图片的速度,我现在的电脑加载某个目录下大约150图片,总计需要30秒的时间,也就是说在这段时间内,这个程序处于卡死状态,用户无法对程序做任何操作。要解决这个问题,就需要用到多线程,把比较耗时的加载图片的操作放置一个单独的子线程里,这样程序界面就始终处于响应操作,用户不用等待全部图片加载完后才能继续操作。

先看下这个程序的头文件

  1. #include <QDialog>  

  2. #include <QPushButton>  

  3. #include <QLineEdit>  

  4. #include "ReadPix.h"  

  5.   

  6. class PreviewPix : public QDialog  

  7. {  

  8.     Q_OBJECT  

  9. private:  

  10.     QString currentDir_String;  

  11.   

  12.     QPushButton* chooseDir_PushButton;  

  13.     QPushButton* pageUp_PushButton;  

  14.     QPushButton* pageDown_PushButton;  

  15.   

  16.     QList<QPushButton*> previewPix_List;  

  17.     QList<QPixmap> pix_List;  

  18.   

  19.     QLineEdit* currentDir_LineEdit;  

  20.   

  21.     ReadPix* readPix_Thread;  

  22. public:  

  23.     PreviewPix(QWidget *parent = 0);  

  24.     ~PreviewPix();  

  25. private slots:  

  26.     void changeCurrentDir();   //注释1  

  27.     void addPixOneByOne(const QPixmap& pix);  

  28. };  

#include <QDialog>
#include <QPushButton>
#include <QLineEdit>
#include "ReadPix.h"

class PreviewPix : public QDialog
{
    Q_OBJECT
private:
    QString currentDir_String;

    QPushButton* chooseDir_PushButton;
    QPushButton* pageUp_PushButton;
    QPushButton* pageDown_PushButton;

    QList<QPushButton*> previewPix_List;
    QList<QPixmap> pix_List;

    QLineEdit* currentDir_LineEdit;

    ReadPix* readPix_Thread;
public:
    PreviewPix(QWidget *parent = 0);
    ~PreviewPix();
private slots:
    void changeCurrentDir();   //注释1
    void addPixOneByOne(const QPixmap& pix);
};

注释1:changeCurrentDir()用于改变程序显示的当前目录,而addPixOneByOne(0函数则用于逐个的添加已经加载完成的图片,这些图片全部在子线程里加载完成的。


      程序构造函数里只有界面布局,所以就不列出来了,这里主要看下这两个函数的实现。

  1. void PreviewPix::changeCurrentDir()  

  2. {  

  3.     QString newDir = QFileDialog::getExistingDirectory(this,tr("Open New Dir"),tr("."));  

  4.     if(newDir.isEmpty())  

  5.         return;  

  6.     else  

  7.         currentDir_String = newDir;  

  8.     currentDir_LineEdit->setText(currentDir_String);  

  9.     pix_List.clear();  

  10.   

  11.     QList<QString> pixNames;  

  12.     QDir dirs(currentDir_String);  

  13.     int cs = dirs.count();  

  14.     for(int i = 0 ; i < cs ; ++i)  

  15.     {  

  16.         QString names = dirs[i];  

  17.         QString nameTail = names.right(3);  

  18.         if(nameTail == tr("png") || nameTail == tr("jpg") || nameTail == tr("bmp")) //注释1  

  19.         {  

  20.             pixNames.append(currentDir_String + tr("//") + names);  //注释2  

  21.         }  

  22.     }  

  23.   

  24.     readPix_Thread->stopLoadPix();  //注释3  

  25.     readPix_Thread->exit();  

  26.     readPix_Thread->setPixName(pixNames);  

  27.     readPix_Thread->start();  

  28. }  

void PreviewPix::changeCurrentDir()
{
    QString newDir = QFileDialog::getExistingDirectory(this,tr("Open New Dir"),tr("."));
    if(newDir.isEmpty())
        return;
    else
        currentDir_String = newDir;
    currentDir_LineEdit->setText(currentDir_String);
    pix_List.clear();

    QList<QString> pixNames;
    QDir dirs(currentDir_String);
    int cs = dirs.count();
    for(int i = 0 ; i < cs ; ++i)
    {
        QString names = dirs[i];
        QString nameTail = names.right(3);
        if(nameTail == tr("png") || nameTail == tr("jpg") || nameTail == tr("bmp")) //注释1
        {
            pixNames.append(currentDir_String + tr("//") + names);  //注释2
        }
    }

    readPix_Thread->stopLoadPix();  //注释3
    readPix_Thread->exit();
    readPix_Thread->setPixName(pixNames);
    readPix_Thread->start();
}

注释1:这里只会加载png,jpg和bmp类型的图片,如果你喜欢,也可以添加其他类型的图片.

注释2:这里需要合成一个完成的图片路径,注意中间的连接符tr("//"),这个是在linux下使用的,因为linux的路径都是“/”所以需要用下转义,如果在windows下编译,需要改成tr("\"),当然你也可以使用前面介绍过的Q_OS_LINUX和Q_OS_WIN等宏,使得代码不需要更改就可以在各个平台编译。这里将全部符合要求的图片的路径放入一个QList<QString>里面,然后下面交给子线程逐个加载.

注释3:这里开启子线程,在线程里开始加载图片

         然后是程序的关键部分,Qt提供了QThread类来作为各平台上通用的线程支持。说的简单些,该类用于创建一个子线程,而且可以在各个平台上使用,该类有个run()保护函数,当线程启动时run()函数就会执行,所以一般都把需要的操作放置run()函数里。

         我们先看下加载图片线程类的头文件

  1. class ReadPix : public QThread  

  2. {  

  3.     Q_OBJECT  

  4. private:  

  5.     QList<QString> pixName_List;  //注释1  

  6.     bool isLoadPix_bool;  

  7. public:  

  8.     ReadPix(QObject* parent = 0);  

  9.     void setPixName(const QList<QString>& pixNames);  //注释2  

  10.     void stopLoadPix();  //注释3  

  11. protected:  

  12.     void run();  

  13. signals:  

  14.     void loadPix(const QPixmap& pix);  //注释4  

  15. };  

class ReadPix : public QThread
{
    Q_OBJECT
private:
    QList<QString> pixName_List;  //注释1
    bool isLoadPix_bool;
public:
    ReadPix(QObject* parent = 0);
    void setPixName(const QList<QString>& pixNames);  //注释2
    void stopLoadPix();  //注释3
protected:
    void run();
signals:
    void loadPix(const QPixmap& pix);  //注释4
};

注释1:该类继承自QThread,这里添加了一个链表用于存放需要加载的图片路径,而布尔值isLoadPix_bool用于标记是否需要继续加载图片,这个值用于控制线程的启动/停止.

注释2:该函数会在主线程里调用,用于给子线程提供需要加载图片的路径

注释3:该函数用于停止线程加载图片的动作,他会把isLoadPix_bool设为false.

注释4:加载图片时,每加载一幅图片,就会以信号的形式发射给主线程,这个信号和主线程的addPixOneByOne()连接。

  1. ReadPix::ReadPix(QObject* parent):QThread(parent),loadPix_Mutex()  

  2. {  

  3.     isLoadPix_bool = false;  //注释1  

  4. }  

  5.   

  6. void ReadPix::setPixName(const QList<QString>& pixNames)  

  7. {  

  8.     pixName_List = pixNames;  

  9. }  

  10.   

  11. void ReadPix::stopLoadPix()  

  12. {  

  13.     isLoadPix_bool = false;  

  14. }  

ReadPix::ReadPix(QObject* parent):QThread(parent),loadPix_Mutex()
{
    isLoadPix_bool = false;  //注释1
}

void ReadPix::setPixName(const QList<QString>& pixNames)
{
    pixName_List = pixNames;
}

void ReadPix::stopLoadPix()
{
    isLoadPix_bool = false;
}

注释1:线程产生是还没有开始加载图片,所以这里值设为false,构造函数和下面的两个函数都很简单。这个类里还有个QMutex类,这个类先暂时无视。

然后是比较关键的run()函数.

  1. void ReadPix::run()  

  2. {  

  3.     isLoadPix_bool = true;  //注释1  

  4.     for(auto A : pixName_List)  

  5.     {  

  6.         if(isLoadPix_bool == false)  //注释2  

  7.             break;  

  8.         QPixmap pix(A);  

  9.         emit loadPix(pix);  //注释3  

  10.     }  

  11. }  

void ReadPix::run()
{
    isLoadPix_bool = true;  //注释1
    for(auto A : pixName_List)
    {
        if(isLoadPix_bool == false)  //注释2
            break;
        QPixmap pix(A);
        emit loadPix(pix);  //注释3
    }
}

注释1:在主线程里调用start()函数启动子线程的加载行为时,也就是启动了run()函数,所以在该函数的最开始将isLoadPix_bool值设为true.

注释2:图片每次都是加载一幅,而每次加载前需要确定是否需要加载,因为有可能主线程需要更换加载的目录,这个时候需要停止子线程的加载行为,然后给子线程一个新的QList<QString>。

注释3:每次加载完一幅图片,就以信号的形式发送给主线程,这样主线程里的界面程序就可以把这幅图片显示出来。由于执行加载图片的代码位于子线程,所以主线程的界面程序会一直处于响应状态。

原文出处

0人推荐
随时随地看视频
慕课网APP