通常情况下,应用程序都是在一个线程中执行操作。但是,当调用一个耗时操作(例如,大批量I/O或大量矩阵变换等CPU密集操作)时,用户界面常常会冻结。而使用
多线程可解决这一问题。
多线程具有以下几点优势。
(1)提高应用程序的响应速度。这对于开发图形界面的程序尤为重要,当一个操作耗时很长时,整个系统都会等待这个操作,程序就不能响应键盘、鼠标、菜单等的操
作,而是使用了多线程技术可将耗时长的操作置于一个新的线程,从而避免以上的问题。
(2)使多CPU系统更加有效。当线程数不大于CPU数目时,操作系统可以调度不同的线程运行于不同的CPU上。
(3)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为独立或半独立的运行部分,这样有利于代码的理解和维护。
多线程程序有以下几个特点。
(1)多线程程序的行为无法预期,当多次执行上述程序时,每一次的运行结构都可能不同。
(2)多线程的执行顺序无法保证,它与操作系统的调度策略和线程优先级等因素有关。
(3)多线程的切换可能发生在任何时刻、任何地点。
(4)多线程对代码的敏感度高,因此对代码的细微修改都可能产生意想不到的结果。
基于以上这些特点,为了有效地使用线程,开发人员必须对其进行控制。
12.1 多线程及简单实例
点击“开始”按钮将启动数个工作线程(工作线程数目由MAXSIZE宏决定),各个线程循环打印数字0-9.直到按下“停止”按钮终止所有线程为止。
运行效果:
threaddlg.h
#ifndef THREADDLG_H
#define THREADDLG_H
#include <QDialog>
#include <QPushButton>
#include "workthread.h"
#define MAXSIZE 5
class ThreadDlg : public QDialog
{
Q_OBJECT
public:
ThreadDlg(QWidget *parent = 0);
~ThreadDlg();
private:
QPushButton *startBtn;
QPushButton *stopBtn;
QPushButton *exitBtn;
public slots:
void slotstart(); //启动线程槽函数
void slotstop(); //终止线程槽函数
private:
WorkThread *workThread[MAXSIZE]; //指向工作线程的私有指针数组,记录了所启动的全部线程
};
#endif // THREADDLG_H
threaddlg.cpp
#include "threaddlg.h"
#include <QHBoxLayout>
ThreadDlg::ThreadDlg(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("线程"));
startBtn = new QPushButton(tr("开始"));
stopBtn = new QPushButton(tr("停止"));
exitBtn = new QPushButton(tr("退出"));
QHBoxLayout *mainLayout = new QHBoxLayout(this);
mainLayout->addWidget (startBtn);
mainLayout->addWidget (stopBtn);
mainLayout->addWidget (exitBtn);
startBtn->setEnabled (true);
stopBtn->setEnabled (true);
connect (startBtn, SIGNAL(clicked(bool)), this, SLOT(slotstart()));
connect (stopBtn, SIGNAL(clicked(bool)), this, SLOT(slotstop()));
connect (exitBtn, SIGNAL(clicked(bool)), this, SLOT(close()));
}
ThreadDlg::~ThreadDlg()
{
}
/*
* 这里使用两个循环,目的是为了使新建的线程尽可能开始执行
*/
void ThreadDlg::slotstart ()
{
for(int i = 0; i < MAXSIZE; i++)
{
workThread[i] = new WorkThread();
}
for(int i = 0; i < MAXSIZE; i++)
{
workThread[i]->start ();
}
startBtn->setEnabled (false);
stopBtn->setEnabled (true);
}
/*
* workThread[i]->terminate ()、 workThread[i]->wait ():调用QThread基类的terminate()函数,依次终止保存在
* workThread[]数组中的WorkThread类实例。但是terminate()函数并不会立刻终止这个线程,该线程何时终止取决于操作系统的
* 调度策略。因此,程序紧挨着调用了QThread基类的wait()函数,它使得线程阻塞等待直到退出或超时。
*/
void ThreadDlg::slotstop ()
{
if(!startBtn->isEnabled())
{
for(int i = 0; i < MAXSIZE; i++)
{
workThread[i]->terminate ();
workThread[i]->wait ();
}
startBtn->setEnabled (true);
stopBtn->setEnabled (false);
}
}
workthread.h
#ifndef WORKTHREAD_H
#define WORKTHREAD_H
#include <QThread>
class WorkThread : public QThread
{
public:
WorkThread();
protected:
void run();
};
#endif // WORKTHREAD_H
workthread.cpp
#include "workthread.h"
#include <QDebug>
WorkThread::WorkThread()
{
}
void WorkThread::run ()
{
while(true)
{
for(int i = 0; i < 10; i++)
{
qDebug() << i << i << i << i << i << i << i << i << i << i;
}
}
}
12.2 多线程控制
本节介绍Qt线程同步互斥控制的基本方法。线程之间存在着互相制约的关系,具体可分为互斥和同步这两种关系。
实现线程的互斥与同步常使用的类有QMutex、QMutexLocker、QReadWriteLocker、QReadLocker、QWriteLocker、QSemaphore和QWaitCondition。
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;
};
这是实现生成从0开始递增并且不允许重复的值得Key类。
在多线程环境下,这个类是不安全的,因为存在多个线程同时修改私有成员key,其结果是不可预知的。
虽然累Key产生主键的函数creatKey()只有一条语句指向修改成员变量key的值,但是C++和“++"操作符并不是原子操作,通常编译后它将被展开成为一下三条机器命令:
将变量值载入寄存器
将寄存器中的值加1
将寄存器中的值写会回主存。
假设当前的key值为0,如果线程1和线程2同时将0值载入寄存器,执行加1操作并将加1后的值写回主存,则结果是两个线程的执行结果将互相覆盖,实际上仅进行了一
次加1操作,此时的key值为1。
为了保证类key在多线程环境下正确执行,上面的三条机器指令必须串行执行且不允许中途被打断(原子操作),即线程1在线程2(或线程2在线程1)之间完整执行上述
三条机器指令。
实际上,私有变量key是一个临近资源(Critical Resource, CR)。临界资源一次仅允许被一个线程使用,它可以是一块内存、一个数据结构、一个文件或者任何其他具有
排他性使用的东西。在程序中,通常竞争使用临界资源。这些必须互斥执行的代码段称为“临界区(Critical Section,CS)”。临界区(代码段)实施对临界资源的操作,
为了阻止问题的产生,一次只能有一个线程进入临界区。通常有关的机制或方法在程序中加上“进入”或“离开”临界区等操作。如果一个线程已经进入某个临界区,则另一
个线程就绝不允许在此刻再进入同一个临界区。
12.2.1 互斥量
1、QMutex类
QMutex类是对互斥量的处理。它被用来保护一段临界区代码,即每次只允许一个线程访问这段代码。
QMutex类的lock()函数用于锁住互斥量。如果互斥量处于解锁状态,则当前线程就会立即抓住并锁定它,否则当前线程就会被阻塞,直到持有这个互斥量的线程对它解
锁。线程调用lock()函数后就会持有这个互斥量,直到调用unlock()操作为止。
QMutex类提供了一个tryLock()函数。如果互斥量已被锁定,则立即返回。
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
;
};
运行界面
#include <QCoreApplication>
#include <QSemaphore>
#include <QThread>
#include <stdio.h>
const int DataSize = 1000;
const int BufferSize = 80;
/*
* 首先生产者向buffer中写入数据,直到它到达终点,然后从起点重新开始覆盖已经存在的数据。消费者读取前者生产的数据,在此处每一个
* int字长都被看成一个资源,实际应用中常会在更大的单位上进行操作,从而减少使用信号量重带来的开销。
*/
int buffer[BufferSize];
/*
* freeBytes信号量控制可被生产者填充的缓冲区部分,被初始化为BufferSize(80),表示程序一开始有BufferSize个缓冲区单元可
* 被填充。
*/
QSemaphore freeBytes(BufferSize);
/*
* usedBytes信号量控制可被消费者读取的缓冲区部分,被初始化为0,表示程序一开始时缓冲区没有数据可供读取。
*/
QSemaphore usedBytes(0);
class Producer : public QThread
{
public:
Producer();
void run();
};
Producer::Producer()
{
}
/*
* freeBytes.acquire ():生产者线程首先获取一个空闲单元,如果此时缓冲区被消费者尚未读取的数据填满,对此函数的调用就会阻塞
* 直到消费者读取了这些数据为止。acquire(n)函数用于获取n个资源,当没有足够的资源时,调用则将被阻塞,直到有足够的可用资源为止
* 除此之外,QSemaphore类还提供了一个tryAcquire(n)函数,在没有足够的资源时,该函数会立即返回。
*
*/
void Producer::run ()
{
for(int i = 0; i < DataSize; i++)
{
freeBytes.acquire ();
buffer[i % BufferSize] = (i % BufferSize);
usedBytes.release ();
}
}
class Consumer : public QThread
{
public:
Consumer();
void run();
};
Consumer::Consumer()
{
}
/*
* usedBytes.acquire ():消费者线程首先获取一个可被读取的单元,如果缓冲区中没有包含任何可以读取的数据,对此函数的调用就会
* 阻碍,直到生产了一些数据为止。
* freeBytes.release ():调用该函数使得这个单元变为空闲,以备生产者下次填充
*/
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 p;
Consumer c;
/*启动生产者消费者线程*/
p.start ();
c.start ();
/*等待生产者和消费者各自执行完毕后自动退出*/
p.wait ();
c.wait ();
return a.exec();
}
12.2.3 线程等待与唤醒
对生产者和消费者问题的另一个解决办法是使用QWaitCondition类,允许线程在一定条件下唤醒其他线程。
#include <QCoreApplication>
#include <stdio.h>
#include <QThread>
#include <QWaitCondition>
#include <QMutex>
const int DataSize = 1000;
const int BufferSize = 80;
int buffer[BufferSize];
QWaitCondition bufferEmpty;
QWaitCondition bufferFull;
QMutex mutex;
int numUseBytes = 0;
int rIndex = 0;
class Producer : public QThread
{
public:
Producer();
void run();
};
Producer::Producer()
{
}
/*
* for(int i = 0; i < DataSize; i++){mutex.lock (); ... mutex.unlock ();}
* for循环中所有语句都需要使用互斥量加以保护,以保证其操作的原子性
* if(numUseBytes == BufferSize: 首先检查缓冲区是否已经填满
* bufferEmpty.wait (&mutex):如果缓冲区已经填满,则等待“缓冲区有空位”条件成立。wait()函数将互斥量解锁并在此等待
* buffer[i % BufferSize] = numUseBytes:如果缓冲区未填满,则向缓冲区中写入一个整数值
* bufferFull.wakeAll ():最后唤醒等待"缓冲区有可用数据"(bufferEmpty变量)条件为“真”的线程
* wakeOne()函数在条件满足时随机唤醒一个等待线程,而wakeAll()函数则在条件满足时唤醒所有等待线程。
*/
void Producer::run ()
{
for(int i = 0; i < DataSize; i++)
{
mutex.lock ();
if(numUseBytes == BufferSize)
{
bufferEmpty.wait (&mutex);
}
buffer[i % BufferSize] = numUseBytes;
++numUseBytes;
bufferFull.wakeAll ();
mutex.unlock ();
}
}
class Consumer : public QThread
{
public:
Consumer();
void run();
};
Consumer::Consumer()
{
}
/*
* bufferFull.wait (&mutex):当缓冲区无数据时,等待“缓冲区有可用数据”条件成立
* bufferEmpty.wakeAll ():唤醒等待“缓冲区有空位”条件的生产者线程
*/
void Consumer::run()
{
forever
{
mutex.lock ();
if(numUseBytes == 0)
{
bufferFull.wait (&mutex);
}
printf ("%ul::[%d] = %d\n", currentThreadId (), rIndex, buffer[rIndex]);
rIndex = (++rIndex) % BufferSize;
--numUseBytes;
bufferEmpty.wakeAll ();
mutex.unlock ();
}
printf ("\n");
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Producer P;
Consumer CA;
Consumer CB;
P.start ();
CA.start ();
CB.start ();
P.wait ();
CA.wait ();
CB.wait ();
return a.exec();
}
12.3多线程应用
本节中通过实现一个多线程的网络时间服务器,介绍如何综合运用多线程技术编程。每当客户请求到达时,服务器将启动一个新线程为它返回当前的时间,服务完毕
后这个线程将自动退出。同时,用户界面会显示当前已接收请求的次数。
12.3.1夫服务器编程
dialog.h
#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;
int count;
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
#include <QGridLayout>
#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 *HBoxLayout = new QHBoxLayout;
HBoxLayout->addStretch (1);
HBoxLayout->addWidget (quitBtn);
HBoxLayout->addStretch (1);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget (label1);
mainLayout->addWidget (label2);
mainLayout->addLayout (HBoxLayout);
connect (quitBtn, SIGNAL(clicked(bool)), 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()));
}
Dialog::~Dialog()
{
}
void Dialog::slotShow ()
{
label2->setText (tr("服务器端口:%1.").arg(++count));
}
timeserver.h
#ifndef TIMERSERVER_H
#define TIMERSERVER_H
#include <QTcpServer>
class Dialog; //服务器端口声明
class TimeServer : public QTcpServer
{
Q_OBJECT
public:
TimeServer(QObject *parent = 0);
protected:
void incomingConnection (int socketDescriptor);
private:
Dialog *dlg;
};
#endif // TIMERSERVER_H
timeserver.cpp
#include "timeserver.h"
#include "timethread.h"
#include "dialog.h"
/*
* Dialog *dlg:用于记录创建这个TCP服务器端对象的父类,这里是界面指针,通过这个指针将线程发出的消息关联到界面的槽函数中
*/
TimeServer::TimeServer(QObject *parent)
:QTcpServer(parent)
{
dlg = (Dialog *)parent;
}
/*
* 重写此数函数。这个函数在TCP服务器端有新的连接时被调用,其参数为所接收新连接的套接字描述符。
*
*/
void TimeServer::incomingConnection (int socketDescriptor)
{
TimeThread *thread = new TimeThread(socketDescriptor, 0);
connect (thread, SIGNAL(finished()), dlg, SLOT(slotShow()));
connect (thread, SIGNAL(finished()), thread, SLOT(deleteLater()), Qt::DirectConnection);
thread->start ();
}
timethread.h
#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
timethread.cpp
#include "timethread.h"
#include <QDateTime>
#include <QByteArray>
#include <QDataStream>
TimeThread::TimeThread(int socketDescriptor, QObject *parent)
:QThread(parent), socketDescriptor(socketDescriptor)
{
}
/*
* TimeThread::run ()函数是工作线程(TimeThread)的实质所在,当在TimeServer::incomingConnection()函数中调用了
* thread->start()函数后,次数函数开始执行
*/
void TimeThread::run ()
{
QTcpSocket tcpSocket; //创建一个QTcpSocket类
/*将以上创建的QTcpSocket类置以从构造函数中传入的套接字描述符,用于向客服端传回服务器端的当前时间*/
if(!tcpSocket.setSocketDescriptor (socketDescriptor))
{
emit error (tcpSocket.error ());
return;
}
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
out.setVersion (QDataStream::Qt_4_3);
uint time2u = QDateTime::currentDateTime ().toTime_t ();
out << time2u;
tcpSocket.write (block); //将获得当前时间传回客户端
tcpSocket.disconnectFromHost (); //端口连接
tcpSocket.waitForDisconnected (); //等待返回
}
12.3.2 客户端编程
运行界面如下
timeclient.h
#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 *dataTimeEdit;
QLabel *stateLabel;
QPushButton *getBtn;
QPushButton *quitBtn;
uint time2u;
QTcpSocket *tcpsocket;
};
#endif // TIMECLIENT_H
timeclient.cpp
#include "timeclient.h"
#include <QGridLayout>
#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);
dataTimeEdit = new QDateTimeEdit(this);
QHBoxLayout *layout1 = new QHBoxLayout;
layout1->addWidget (dataTimeEdit);
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 (1);
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(bool)), this, SLOT(getTime()));
connect(quitBtn, SIGNAL(clicked(bool)), this, SLOT(close()));
tcpsocket = new QTcpSocket(this);
connect (tcpsocket, SIGNAL(readyRead()), this, SLOT(getTime()));
connect (tcpsocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(showError(QAbstractSocket::SocketError)));
portLineEdit->setFocus ();
}
TimeClient::~TimeClient()
{
}
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_4_3);
if(time2u == 0)
{
if(tcpsocket->bytesAvailable () < (int)sizeof(uint))
{
return;
}
in >> time2u;
}
dataTimeEdit->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 ()));
break;
}
getBtn->setEnabled (true);
}