十二.QT5多线程
一、多线程的基础知识
1、进程的定义:进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
2、线程的定义:引入进程的目的是为了更好地使多道程序并发执行,提高资源利用率和系统吞吐量;而引入线程的目的则是为了减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能。线程最直接的理解就是“轻量级进程”,它是一个基本的CPU执行单元,也是程序执行流的最小单元,由线程ID、程序计数器、寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的.资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。
3、多线程的优势
(1)提高应用程序的响应速度。这对于开发图形界面的程序尤为重要,当一个操作耗时很长时,整个系统都会等待这个操作,程序就不能响应键盘、鼠标、菜单等的操作,而使用多线程技术可将耗时长的操作置于一个新的线程,从而避免出现以上问题。
(2)使多CPU系统更加有效。当线程数不大于CPU数目时,操作系统可以调度不同的线程运行于不同的CPU上。
(3)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为独立或半独立的运行部分,这样有利于代码的理解和维护。
4、多线程程序具有以下特点
(1)多线程程序的行为无法预期,当多次执行上述程序时,每次的运行结果都可能不同。
(2)多线程的执行顺序无法保证,它与操作系统的调度策略和线程优先级等因素有关。
(3)多线程的切换可能发生在任何时刻、任何地点。
(4)由于多线程对代码的敏感度高,因此对代码的细微修改都可能产生意想不到的结果。
二、多线程及简单实例
如图所示,单击“开始”按钮将启动数个工作线程(工作线程数目由MAXSIZE宏决定),各个线程循环打印数字0~9,直到单击“停止”按钮终止所有线程为止。
具体步骤如下。
创建qt应用程序项目,类:QDialog
(1)在头文件“threaddlg.h”中声明用于界面显示所需的控件,其具体代码如下:
#include <QDialog> #include <QPushButton> class ThreadDlg : public QDialog { Q_OBJECT public: ThreadDlg(QWidget *parent = 0); ~ThreadDlg(); private: QPushButton *startBtn;//开始按钮部件 QPushButton *stopBtn;//停止按钮部件 QPushButton *quitBtn;//退出按钮部件 };
(2)在源文件“threaddlg.cpp”的构造函数中,完成各个控件的初始化工作,其具体代码如下:
#include "threaddlg.h" #include <QHBoxLayout> ThreadDlg::ThreadDlg(QWidget *parent) : QDialog(parent) { setWindowTitle(tr("线程"));//设置主窗口标题 startBtn = new QPushButton(tr("开始"));//设置按钮文本 stopBtn = new QPushButton(tr("停止")); quitBtn = new QPushButton(tr("退出")); QHBoxLayout *mainLayout = new QHBoxLayout(this);//设置水平布局 mainLayout->addWidget(startBtn);//将按钮从左到右放入mainLayout mainLayout->addWidget(stopBtn); mainLayout->addWidget(quitBtn); }
(3)此时运行程序,界面显示如图所示
以上完成了界面的设计,下面的内容是具体的功能实现。
(1)在头文件“workthread.h”中,工作线程WorkThread类继承自QThread类。重新实现run()函数。其具体代码如下:
#include <QThread> class WorkThread : public QThread { Q_OBJECT public: WorkThread(); protected: void run(); };
(2)在源文件“workthread.cpp”中添加具体实现代码如下:
#include "workthread.h" #include <QtDebug> WorkThread::WorkThread() { }
//run()函数实际上是一个死循环,它不停地打印数字0~9。为了显示效果明显,
//程序将每一个数字重复打印8次。 void WorkThread::run() { while(true) { for(int n=0;n<10;n++) qDebug()<<n<<n<<n<<n<<n<<n<<n<<n; } }
(3)在头文件“threaddlg.h”中添加以下内容:
#include "workthread.h" #define MAXSIZE 1 //MAXSIZE宏定义了线程的数目 public slots: void slotStart(); //槽函数用于启动线程 void slotStop(); //槽函数用于终止线程 private: WorkThread *workThread[MAXSIZE]; //WorkThread *workThread[MAXSIZE]:指向工作线程(WorkThread)的私有指针数组workThread,记录了所启动的全部线程。
(4)在源文件“threaddlg.cpp”中添加以下内容。 其中,在构造函数中添加如下代码:
connect(startBtn,SIGNAL(clicked()),this,SLOT(slotStart())); connect(stopBtn,SIGNAL(clicked()),this,SLOT(slotStop())); connect(quitBtn,SIGNAL(clicked()),this,SLOT(close()));
当用户单击“开始”按钮时,将调用槽函数slotStart()。这里使用两个循环,目的是使新建的线程尽可能同时开始执行,其具体实现代码如下:
void ThreadDlg::slotStart() { for(int i=0;i<MAXSIZE;i++) { workThread[i]=new WorkThread();//创建指定数目的WorkThread线程,并将WorkThread实例的指针保存在指针数组workThread中。 } for(int i=0;i<MAXSIZE;i++) { workThread[i]->start();//调用QThread基类的start()函数,此函数将启动run()函数,从而使线程开始真正运行。 } startBtn->setEnabled(false); stopBtn->setEnabled(true); }
当用户单击“停止”按钮时,将调用槽函数slotStop()。其具体实现代码如下:
void ThreadDlg::slotStop() { for(int i=0;i<MAXSIZE;i++) { workThread[i]->terminate(); workThread[i]->wait(); } startBtn->setEnabled(true); stopBtn->setEnabled(false); }
(5)多线程简单实现结果如图12.2所示。 第1列是启动5个线程的运行结果,第2列是启动单一线程的运行结果。可以看出,单一线程的输出是顺序打印的,而多线程的输出结果则是乱序打印的,这正是多线程的一大特点。
三、多线程控制
线程中存在着相互制约的关系,互斥和同步。
实现线程的互斥与同步常使用的类有QMutex、QMutexLocker、QReadWriteLocker、QReadLocker、QWriteLocker、QSemaphore和QWaitCondition。 下面举一个例子加以说明:
class Key { public: Key() {key=0;} int creatKey() {++key; return key;} int value()const {return key;} private: int key; };
在多线程环境下,这个类是不安全的,因为存在多个线程同时修改私有成员key,其结果是不可预知的。
虽然Key类产生主键的函数creatKey()只有一条语句执行修改成员变量key的值,但是C++的“++”操作符并不是原子操作,通常编译后,它将被展开成为以下三条机器命令:
将变量值载入寄存器。
将寄存器中的值加1。
将寄存器中的值写回主存。
1、互斥量
QMutex类
QMutex类是对互斥量的处理。它被用来保护一段临界区代码,即每次只允许一个线程访问这段代码。
QMutex类的lock()函数用于锁住互斥量。如果互斥量处于解锁状态,则当前线程就会立即抓住并锁定它,否则当前线程就会被阻塞,直到持有这个互斥量的线程对它解锁。线程调用lock()函数后就会持有这个互斥量,直到调用unlock()操作为止。 QMutex类还提供了一个tryLock()函数。如果互斥量已被锁定,则立即返回。 例如:
class Key { public: Key() {key=0;} int creatKey() { mutex.lock(); ++key; return key; mutex. unlock();} int value()const { mutex.lock(); return key; mutex.unlock();} private: int key; QMutex mutex; };
QMutexLocker类
Qt提供的QMutexLocker类可以简化互斥量的处理,它在构造函数中接收一个QMutex对象作为参数并将其锁定,在析构函数中解锁这个互斥量,这样就解决了以上问题。 例如:
class Key { public: Key() {key=0;} int creatKey() { QmutexLocker locker(&mutex); ++key; return key; } int value()const { QmutexLocker locker(&mutex); return key; } private: int key; QMutex mutex; };
locker()函数作为局部变量会在函数退出时结束其作用域,从而自动对互斥量mutex解锁。
2、信号量
信号量可以理解为对互斥量功能的扩展,互斥量只能锁定一次而信号量可以获取多次,它可以用来保护一定数量的同种资源。信号量的典型用例是控制生产者/消费者之间共享的环形缓冲区。
生产者/消费者实例中对同步的需求有两处:
(1)如果生产者过快地生产数据,将会覆盖消费者还没有读取的数据。
(2)如果消费者过快地读取数据,将越过生产者并且读取到一些过期数据。
针对以上问题,有两种解决方法:
(1)首先使生产者填满整个缓冲区,然后等待消费者读取整个缓冲区,这是一种比较笨拙的方法。
(2)使生产者和消费者线程同时分别操作缓冲区的不同部分,这是一种比较高效的方法。
2.1基于控制台程序实现。
(1)在源文件“main.cpp”中添加的具体实现代码如下:
#include <QCoreApplication> #include <QSemaphore> #include <QThread> #include <stdio.h> const int DataSize=1000; const int BufferSize=80; int buffer[BufferSize]; //首先,生产者向buffer中写入数据,直到它到达终点,然后从起点重新开始覆盖已经存在的数据 QSemaphore freeBytes(BufferSize); //freeBytes信号量控制可被生产者填充的缓冲区部分,被初始化为ButterSize(80),表示程序一开始时有BufferSize个缓冲区单元可被填充 QSemaphore usedBytes(0);//usedByters信号量控制可被消费者读取的缓冲区部分
//Producer类继承自QThread类,作为生产者类,其声明如下:
class Producer : public QThread { public: Producer(); void run(); }; Producer::Producer() { } void Producer::run() { for(int i=0;i<DataSize;i++) { freeBytes.acquire(); //生产者线程首先获取一个空闲单元 buffer[i%BufferSize]=(i%BufferSize);//一旦生产者获取了某个空闲单元,就使用当前的缓冲区单元序号填写这个缓冲区单元 usedBytes.release();//释放资源 } }
//Consumer类继承自QThread类,作为消费者类,其声明如下: class Consumer : public QThread { public: Consumer(); void run(); }; Consumer::Consumer() { } void Consumer::run() { for(int i=0;i<DataSize;i++) { usedBytes.acquire();//消费者线程首先获取一个可被读取的单元,如果缓冲区中没有包含任何可以读取的数据,对此函数的调用就会阻塞,直到生产者生产了一些数据为止。 fprintf(stderr,"%d",buffer[i%BufferSize]);//一旦消费者获取了这个单元,会将这个单元的内容打印出来。
if(i%16==0&&i!=0) fprintf(stderr,"\n"); freeBytes.release();//使单元变为空闲 } fprintf(stderr,"\n"); } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); Producer producer; Consumer consumer; /* 启动生产者和消费者线程 */
producer.start();
consumer.start();
/* 等待生产者和消费者各自执行完毕后自动退出 */
producer.wait(); consumer.wait(); return a.exec(); }
最终运行结果如图所示。
3、线程等待与唤醒
使用QWaitCondition类解决生产者和消费者问题。
源文件“main.cpp”的具体内容如下:
#include <QCoreApplication> #include <QWaitCondition> #include <QMutex> #include <QThread> #include <stdio.h> const int DataSize=1000; const int BufferSize=80; int buffer[BufferSize]; QWaitCondition bufferEmpty; QWaitCondition bufferFull; QMutex mutex; //使用互斥量保证对线程操作的原子性 int numUsedBytes=0; //变量numUsedBytes表示存在多少“可用字节” int rIndex=0; //本例中启动了两个消费者线程,并且这两个线程读取同一个缓冲区,为了不重复读取,设置全局变量rIndex用于指示当前所读取缓冲区位置。 class Producer : public QThread { public: Producer(); void run(); }; Producer::Producer() { } void Producer::run() { for(int i=0;i<DataSize;i++) //for循环中的所有语句都需要使用互斥量加以保护,以保证其操作的原子性。 { mutex.lock(); if(numUsedBytes==BufferSize) //首先检查缓冲区是否已被填满。 bufferEmpty.wait(&mutex); //如果缓冲区已被填满,则等待“缓冲区有空位”(bufferEmpty变量)条件成立。wait()函数将互斥量解锁并在此等待,其原型如下: buffer[i%BufferSize]=numUsedBytes; //如果缓冲区未被填满,则向缓冲区中写入一个整数值。 ++numUsedBytes; //增加numUsedBytes变量 bufferFull.wakeAll(); //最后唤醒等待“缓冲区有可用数据”(bufferEmpty变量)条件为“真”的线程。 mutex.unlock(); } } class Consumer : public QThread { public: Consumer(); void run(); }; Consumer::Consumer() { } void Consumer::run() { forever { mutex.lock(); if(numUsedBytes==0) bufferFull.wait(&mutex); //当缓冲区中无数据时,等待“缓冲区有可用数据”(bufferFull变量)条件成立。 printf("%ul::[%d]=%d\n",currentThreadId(),rIndex,buffer[rIndex]); //当缓冲区中有可用数据即条件成立时,打印当前线程号和rIndex变量,以及其指示的当前可读取数据。这里为了区分究竟是哪一个消费者线程消耗了缓冲区里的数据,使用了QThread类的currentThreadId()静态函数输出当前线程的ID。这个ID在X11环境下是一个unsigned long 类型的值。 rIndex=(++rIndex)%BufferSize; //将rIndex变量循环加1 --numUsedBytes; //numUsedBytes变量减1,即可用的数据减1。 bufferEmpty.wakeAll();//唤醒等待“缓冲区有空位”(bufferEmpty变量)条件的生产者线程。 mutex.unlock(); } printf("\n"); } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); Producer producer; Consumer consumerA; Consumer consumerB; producer.start(); consumerA.start(); consumerB.start(); producer.wait(); consumerA.wait(); consumerB.wait(); return a.exec(); }
四、综合应用-----多线程应用
1、“实例”服务器端编程
首先,建立服务器端工程“TimeServer.pro”。文件代码如下。
(1)在头文件“dialog.h”中,定义服务器端界面类Dialog继承自QDialog类,其具体代码如下:
#ifndef DIALOG_H #define DIALOG_H #include <QDialog> #include <QLabel> #include <QPushButton> class TimeServer; class Dialog : public QDialog { Q_OBJECT public: Dialog(QWidget *parent = 0); ~Dialog(); public slots: void slotShow(); //此槽函数用于界面上显示的请求次数 private: QLabel *Label1; //此标签用于显示监听端口 QLabel *Label2; //此标签用于显示请求次数 QPushButton *quitBtn; //退出按钮 TimeServer *timeServer; //TCP服务器端timeServer int count; //请求次数计数器count }; #endif // DIALOG_H
(2)在源文件“dialog.cpp”中,Dialog类的构造函数完成了初始化界面,其具体代码如下:
#include "dialog.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QMessageBox>
#include "timeserver.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("多线程时间服务器"));
Label1 =new QLabel(tr("服务器端口:"));
Label2 = new QLabel;
quitBtn = new QPushButton(tr("退出"));
QHBoxLayout *BtnLayout = new QHBoxLayout;
BtnLayout->addStretch(1);
BtnLayout->addWidget(quitBtn);
BtnLayout->addStretch(1);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(Label1);
mainLayout->addWidget(Label2);
mainLayout->addLayout(BtnLayout);
connect(quitBtn,SIGNAL(clicked()),this,SLOT(close()));
count=0;
timeServer = new TimeServer(this);
if(!timeServer->listen())
{
QMessageBox::critical(this,tr("多线程时间服务器"),
tr("无法启动服务器:%1.").arg(timeServer->errorString()));
close();
return;
}
Label1->setText(tr("服务器端口:%1.").arg(timeServer->serverPort()));
}
void Dialog::slotShow()
{
Label2->setText(tr("第%1次请求完毕。").arg(++count));
}
Dialog::~Dialog()
{
}
(3)在服务器端工程“TimeServer.pro”中,添加C++ Class文件“timethread.h”及“timethread.cpp”。在头文件“timethread.h”中,工作线程TimeThread类继承自QThread类,实现TCP套接字,其具体代码如下:
#ifndef TIMETHREAD_H #define TIMETHREAD_H #include <QThread> #include <QtNetwork> #include <QTcpSocket> class TimeThread : public QThread { Q_OBJECT public: TimeThread(int socketDescriptor,QObject *parent=0); void run(); //重写此虚函数 signals: void error(QTcpSocket::SocketError socketError); //出错信号 private: int socketDescriptor; //套接字描述符 }; #endif // TIMETHREAD_H
#include "timethread.h" #include <QDateTime> #include <QByteArray> #include <QDataStream> TimeThread::TimeThread(int socketDescriptor,QObject *parent):QThread(parent),socketDescriptor(socketDescriptor) { } void TimeThread::run() { QTcpSocket tcpSocket; //创建一个QTcpSocket类 if(!tcpSocket.setSocketDescriptor(socketDescriptor)) //将以上创建的QTcpSocket类置以从构造函数中传入的套接字描述符,用于向客户端传回服务器端的当前时间。
{ emit error(tcpSocket.error()); //如果出错,则发出error(tcpSocket.error())信号报告错误。 return; } QByteArray block; QDataStream out(&block,QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_5_8); uint time2u = QDateTime::currentDateTime().toTime_t(); //如果不出错,则开始获取当前时间。 out<<time2u; tcpSocket.write(block); //将获得的当前时间传回客户端 tcpSocket.disconnectFromHost(); //断开连接 tcpSocket.waitForDisconnected(); //等待返回 }
(4)在服务器端工程“TimeServer.pro”中添加C++ Class文件“timeserver.h”及“timeserver.cpp”。在头文件“timeserver.h”中,实现了一个TCP服务器端,TimeServer类继承自QTcpServer类,其具体代码如下:
#ifndef TIMESERVER_H #define TIMESERVER_H #include <QTcpServer> class Dialog; //服务器端的声明 class TimeServer : public QTcpServer { Q_OBJECT public: TimeServer(QObject *parent=0); protected: void incomingConnection(int socketDescriptor); //重写此虚函数。这个函数在TCP服务器端有新的连接时被调用,其参数为所接收新连接的套接字描述符。 private: Dialog *dlg; //用于记录创建这个TCP服务器端对象的父类,这里是界面指针,通过这个指针将线程发出的消息关联到界面的槽函数中。 }; #endif // TIMESERVER_H
#include "timeserver.h" #include "timethread.h" #include "dialog.h" TimeServer::TimeServer(QObject *parent):QTcpServer(parent) { dlg =(Dialog *)parent; } void TimeServer::incomingConnection(int socketDescriptor) { TimeThread *thread = new TimeThread(socketDescriptor,0); //以返回的套接字描述符socketDescriptor创建一个工作线程TimeThread。 connect(thread,SIGNAL(finished()),dlg,SLOT(slotShow())); //将上述创建的线程结束消息函数finished()关联到槽函数slotShow()用于显示请求计数。此操作中,因为信号是跨线程的,所以使用了排队连接方式。 connect(thread,SIGNAL(finished()),thread,SLOT(deleteLater()), Qt::DirectConnection); //将上述创建的线程结束消息函数finished()关联到线程自身的槽函数 deleteLater()用于结束线程。 thread->start(); //启动上述创建的线程。执行此语句后,工作线程(TimeThread)的虚函数run()开始执行。 }
(5)在服务器端工程文件“TimeServer.pro”中添加如下代码: QT += network
2、“实例”客户端编程
客户端界面如图所示。
操作步骤如下。
(1)建立客户端工程“TimeClient.pro”。在头文件“timeclient.h”中,定义了客户端界面类TimeClient继承自QDialog类
#ifndef TIMECLIENT_H #define TIMECLIENT_H #include <QDialog> #include <QLabel> #include <QLineEdit> #include <QPushButton> #include <QDateTimeEdit> #include <QTcpSocket> #include <QAbstractSocket> class TimeClient : public QDialog { Q_OBJECT public: TimeClient(QWidget *parent = 0); ~TimeClient(); public slots: void enableGetBtn(); void getTime(); void readTime(); void showError(QAbstractSocket::SocketError socketError); private: QLabel *serverNameLabel; QLineEdit *serverNameLineEdit; QLabel *portLabel; QLineEdit *portLineEdit; QDateTimeEdit *dateTimeEdit; QLabel *stateLabel; QPushButton *getBtn; QPushButton *quitBtn; uint time2u; QTcpSocket *tcpSocket; }; #endif // TIMECLIENT_H
(2)在源文件“timeclient.cpp”中,TimeClient类的构造函数完成了初始化界面,其具体代码。 在源文件“timeclient.cpp”中,enableGetBtn()函数的具体代码如下:
#include "timeclient.h" #include <QHBoxLayout> #include <QVBoxLayout> #include <QGridLayout> #include <QDataStream> #include <QMessageBox> TimeClient::TimeClient(QWidget *parent) : QDialog(parent) { setWindowTitle(tr("多线程时间服务客户端")); serverNameLabel =new QLabel(tr("服务器名:")); serverNameLineEdit = new QLineEdit("Localhost"); portLabel =new QLabel(tr("端口:")); portLineEdit = new QLineEdit; QGridLayout *layout = new QGridLayout; layout->addWidget(serverNameLabel,0,0); layout->addWidget(serverNameLineEdit,0,1); layout->addWidget(portLabel,1,0); layout->addWidget(portLineEdit,1,1); dateTimeEdit = new QDateTimeEdit(this); QHBoxLayout *layout1 = new QHBoxLayout; layout1->addWidget(dateTimeEdit); stateLabel =new QLabel(tr("请首先运行时间服务器!")); QHBoxLayout *layout2 = new QHBoxLayout; layout2->addWidget(stateLabel); getBtn = new QPushButton(tr("获取时间")); getBtn->setDefault(true); getBtn->setEnabled(false); quitBtn = new QPushButton(tr("退出")); QHBoxLayout *layout3 = new QHBoxLayout; layout3->addStretch(); layout3->addWidget(getBtn); layout3->addWidget(quitBtn); QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->addLayout(layout); mainLayout->addLayout(layout1); mainLayout->addLayout(layout2); mainLayout->addLayout(layout3); connect(serverNameLineEdit,SIGNAL(textChanged(QString)), this,SLOT(enableGetBtn())); connect(portLineEdit,SIGNAL(textChanged(QString)), this,SLOT(enableGetBtn())); connect(getBtn,SIGNAL(clicked()),this,SLOT(getTime())); connect(quitBtn,SIGNAL(clicked()),this,SLOT(close())); tcpSocket = new QTcpSocket(this); connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(readTime())); connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(showError(QAbstractSocket::SocketError))); portLineEdit->setFocus(); } void TimeClient::enableGetBtn() { getBtn->setEnabled(!serverNameLineEdit->text().isEmpty()&& !portLineEdit->text().isEmpty()); } void TimeClient::getTime() { getBtn->setEnabled(false); time2u =0; tcpSocket->abort(); tcpSocket->connectToHost(serverNameLineEdit->text(), portLineEdit->text().toInt()); } void TimeClient::readTime() { QDataStream in(tcpSocket); in.setVersion(QDataStream::Qt_5_9); if(time2u==0) { if(tcpSocket->bytesAvailable()<(int)sizeof(uint)) return; in>>time2u; } dateTimeEdit->setDateTime(QDateTime::fromTime_t(time2u)); getBtn->setEnabled(true); } void TimeClient::showError(QAbstractSocket::SocketError socketError) { switch (socketError) { case QAbstractSocket::RemoteHostClosedError: break; case QAbstractSocket::HostNotFoundError: QMessageBox::information(this, tr("时间服务客户端"), tr("主机不可达!")); break; case QAbstractSocket::ConnectionRefusedError: QMessageBox::information(this, tr("时间服务客户端"), tr("连接被拒绝!")); break; default: QMessageBox::information(this, tr("时间服务客户端"), tr("产生如下错误: %1.").arg(tcpSocket->errorString())); } getBtn->setEnabled(true); } TimeClient::~TimeClient() { }