多线程是一个很大的范围,内容也非常的多,我手上就有三本总计超过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);
};
注释1:changeCurrentDir()用于改变程序显示的当前目录,而addPixOneByOne()函数则用于逐个的添加已经加载完成的图片,这些图片全部在子线程里加载完成的。
程序构造函数里只有界面布局,所以就不列出来了,这里主要看下这两个函数的实现。
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:这里开启子线程,在线程里开始加载图片,这几行代码的作用下面详述
这个类还有一个逐个添加图片的槽,这个槽会和子线程的信号连接,子线程每加载一幅图片,就把图片发给这个槽。
void PreviewPix::addPixOneByOne(const QPixmap& pix)
{
pix_List.append(pix);
int cs = pix_List.count() - 1;
if(cs < PIX_COUNT*PIX_COUNT) //注释1
{
QPixmap pPix = pix.scaled(PIX_SIZE,PIX_SIZE);
previewPix_List.at(cs)->setIcon(QIcon(pPix));
previewPix_List.at(cs)->setIconSize(QSize(PIX_SIZE,PIX_SIZE));
}
}
注释1:这里暂时只显示前面的25张图片。
然后是程序的关键部分,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
};
注释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;
}
注释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
}
}
注释1:在主线程里调用start()函数启动子线程的加载行为时,也就是启动了run()函数,所以在该函数的最开始将isLoadPix_bool值设为true.
注释2:图片每次都是加载一幅,而每次加载前需要确定是否需要加载,因为有可能主线程需要更换加载的目录,这个时候需要停止子线程的加载行为,然后给子线程一个新的QList<QString>。
注释3:每次加载完一幅图片,就以信号的形式发送给主线程,这样主线程里的界面程序就可以把这幅图片显示出来。由于执行加载图片的代码位于子线程,所以主线程的界面程序会一直处于响应状态。
在看完了子线程的类后,再回过头来看下主线程里的代码,在主程序里的changeCurrentDir()函数的最后有这样几行代码
readPix_Thread->stopLoadPix(); //注释1
readPix_Thread->exit(); //注释2
readPix_Thread->setPixName(pixNames);
readPix_Thread->start();
注释1:changeCurrentDir()函数在主程序的作用是改变目录,也就是改变需要加载的图片,这里有一种情况,当程序正在逐个加载图片而且没有加载完成,而用户又选择了另一个人目录,这个时候子线程要做的是更换加载的图片路径。这里首先使得线程停下来。
注释2:这里使用线程退出,这回释放线程像系统申请的资源,就我们这个例子而言,这里没有申请资源所以也就没必要释放了,但出于良好的编程习惯,这里调用exit()还是非常有必要的.
最后还有的是这个程序的析构函数。
PreviewPix::~PreviewPix()
{
readPix_Thread->stopLoadPix();
readPix_Thread->exit();
}
在前面的例子里,由于Qt自动管理内存的机智,所以大部分的类都没有写析构函数,但这个类析构函数就无能使用默认的了,在用户加载大量图片的过程中,用户很可能不等待加载完成就直接关闭了程序,析构函数的代码就是应为这个问题.
程序到这里看似完成,但如果你多次运行时很容易出现各种奇怪的错误,这里牵扯到线程间同步与通信的问题。再次看下线程类ReadPix的run()函数就很容易发现问题,这个函数里有个循环,而这个循环的可能会随时结束,结束的实现取决于主线程调用stopLoadPix()函数的实现,而主线程希望在调用stopLoadPix()后就不会在接收到loadPix(const QPixmap&)信号,否则的话,主界面更换加载图片的目录时候,第一幅图片是上一个目录的某张图片,甚至更糟,由于主线程加载的图片数量和目录下图片的数量不一致,这回导致极难排查的越界错误。问题的根源在于加载图片的循环里,每次循环都需要执行三行代码。
if(isLoadPix_bool == false) break;
QPixmap pix(A);
emit loadPix(pix);
如果主线程在第一行代码执行后调用stopLoadPix()函数,这个时候isLoadPix_bool的值已经变为false,但由于第一行代码已经执行完成了,所以下面两行函数任然会执行,这就会引起前面所说的问题,对于这里情况,Qt提供了QMutex类,可以将这三行代码变成原子操作,QMetux最主要的两个函数就是lock()和unlock()。所以加载函数里的for循环加上互斥锁后就可以避免这类问题.
for(auto A : pixName_List)
{
loadPix_Metex.lock();
if(isLoadPix_bool == false)
break;
QPixmap pix(A);
emit loadPix(pix);
loadPix_Mutex.unloack();
}
如果电脑的硬件够好,对于上面的例子来说采用多个线程一起加载图片是个不错的注意,上面的例子只采用了一个线程,而如果采用多个线程的话可以极大的提高加载图片的速度。但这里引起另一个问题,就是主线程里的addPixOneByOne()函数,这个函数的第前两行代码是这样的.
pix_List.append(pix);
int cs = pix_List.count() - 1;
这个函数会往链表里添加一个图片,然后计算已经加载的图片总是,如果这个函数只有一个线程调用,那没有什么问题,但如果为了更快的加载图片,程序使用了两个线程,线程A和线程B来加载图片,而这两个线程都会和这个槽连接,那当线程A发射信号是,执行完第一行代码是,线程B巧合也加载完一幅图片,这时候线程B也调用了这个函数并向链表里添加了一个图片,那线程A调用的函数在计算链表的count()就会得出错误的结果,这个问题说明addPixOneByOne()并不是一个线程安全的函数,方法是使用上面的互斥锁,将这两行代码合并成一个原子操作,这样整个函数就变成了线程安全函数.