多线程是一个很大的范围,内容也非常的多,我手上就有三本总计超过1500页的书讲述多线程的问题,这一章只能演示下Qt对多线程提供的一些支持。多线程有很强的平台相关性,很多时候需要用到个平台的API,这对于移植提出了挑战,而Qt提供的线程相关的类可以在各个平台上使用,对于很多开发者来说,这可以节约很多时间和精力。
本章只讨论Qt的线程的一些类及用法,这里假设你已经对线程有一定的了解,类似线程,互斥锁,互斥量,死锁,线程安全,原子操作等一些术语假设你已经了解他们的含义,这里不多做介绍了。
在第二十一章里有个显示本地目录的演示程序,这个程序遇到一个问题,由于需要加载的目录数量过多,程序启动需要花费大量时间,为了在在Linux下运行,甚至取消了有大量文件的两个目录,当然对于一个用于演示的程序来说,这到没有什么太大关系,但这里的真正问题在于:当程序直线某个比较费时的操作时,界面就会卡死。
这里有个很简单的演示程序,这个程序有个选择本地目录的功能,当用户选择了某个目录的话,这个程序就可以显示该目录下所有图片的缩略图,但假如一个目录下的图片比较多,单幅图片比较大时,界面就会处于卡死状态,卡死的时间取决于加载图片的速度,我现在的电脑加载某个目录下大约150图片,总计需要30秒的时间,也就是说在这段时间内,这个程序处于卡死状态,用户无法对程序做任何操作。要解决这个问题,就需要用到多线程,把比较耗时的加载图片的操作放置一个单独的子线程里,这样程序界面就始终处于响应操作,用户不用等待全部图片加载完后才能继续操作。
先看下这个程序的头文件
#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);
};
#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函数则用于逐个的添加已经加载完成的图片,这些图片全部在子线程里加载完成的。
程序构造函数里只有界面布局,所以就不列出来了,这里主要看下这两个函数的实现。
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();
}
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()函数里。
我们先看下加载图片线程类的头文件
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
};
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()连接。
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;
}
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()函数.
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
}
}
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:每次加载完一幅图片,就以信号的形式发送给主线程,这样主线程里的界面程序就可以把这幅图片显示出来。由于执行加载图片的代码位于子线程,所以主线程的界面程序会一直处于响应状态。