文章目录
- 前言
- 1. 何时使用线程
- 2. QThread类实现多线程
- 2.1 多线程的实现方法
- 2.2 线程休眠
- 2.3 正确结束线程
- 3. 线程同步
- 3.1 互斥量
- 3.2 信号量
- 3.3 条件变量
- 4. 线程池
- 参考资料
前言
程序中调用耗时的操作(如大批量I/O、实时通信、大计算量算法等)会造成用户界面的卡顿,通常将这一类操作放在子线程中来解决这一问题。
1. 何时使用线程
在进行桌面应用程序开发的时候,如果应用程序在某些情况下需要处理比较复杂的逻辑, 如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作,这种情况下就需要使用多线程。在 Qt 中使用多线程应遵循以下原则:
- 遵循前后台分离的设计原则,前台主线程负责窗口事件处理或者窗口控件数据的更新,子线程负责后台的业务逻辑处理,不能对窗口对象做任何操作。
- 线程之间进行数据的传递,使用Qt中的信号槽机制。
2. QThread类实现多线程
2.1 多线程的实现方法
Qt 使用QThread
类实现多线程,具体实现方法有两种:一种是创建继承于QThread
的线程类,并重写run()
方法(不推荐);另一种则是创建继承于QObject
的工作类(Worker Class
),在主线程中通过QObject::moveToThread()
方法将工作类移动到子线程中(推荐)。
1) subclass QThread
注意:这种使用方法并不推荐,Bradley T. Hughes
在 2010
专门写了篇博客 You’re doing it wrong…来讨论QThread
的正确使用方法。
/*------------------------------WorkerThread-----------------------------------*/
class WorkerThread : public QThread
{
Q_OBJECT
void run() override {
qDebug() << "Thread in run is" << QThread::currentThread();
connect(ptcpSocket, &QTcpSocket::readyRead, this, [=]()
{
qDebug() << "Thread in slot is" << QThread::currentThread();
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
});
exec();
}
signals:
void resultReady(const QString &s);
};
/*------------------------------MainWindow-----------------------------------*/
void MainWindow::startWorkInAThread()
{
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, &WorkerThread::resultReady, this, &MainWindow::handleResults);
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
workerThread->start();
}
通过qDebug() << QThread::currentThread();
可以打印当前代码所运行的线程。
在本例中,原本是想在子线程中实现tcp通信的数据接收功能,但实际上打印槽函数运行的线程却是主线程,原因在于Qt的信号槽机制:
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
connect函数中,最后一个形参为连接类型Qt::ConnectionType
,缺省类型为Qt::AutoConnection
。信号发送时,确定连接类型:当信号发送者和接收者在同一线程内,使用Qt::DirectConnection
;当信号发送者和接收者不在同一线程,使用Qt::QueuedConnection
。
Qt::DirectConnection:
信号发送时槽函数立刻被执行,此时槽函数处于发送信号的一线程。
Qt::QueuedConnection:
当控制权回到接收者所在的事件循环时,槽函数才被执行,此时槽函数在接收者所在线程。
因此,在例子中WorkerThread
中的connect
使用Qt::QueuedConnection
作为连接类型。同时由于MainWindow
中定义的workerThread
对象处于主线程,因此槽函数也将在主线程中运行。如果你看过Qt自带的例子,你会发现 QThread
中 slot 和 run函数共同操作的对象,都会用QMutex
锁住。为什么?因为 slot 和 run 处于不同线程,需要线程间的同步!
2) worker-object
Qt 4.4
版本之后完善了线程的亲和性以及信号槽机制,我们有了更为安全的使用线程的方式,即 QObject::moveToThread()
。使用信号和槽时根本不用考虑多线程的存在。也不用使用QMutex
来进行同步,Qt的事件循环会自己自动处理。
/*---------------------------Worker----------------------------*/
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(){
pTimer = new QTimer;
connect(pTimer,&QTimer::timeout,this,&Worker::onTimeout);
}
public slots:
void doWork(const QString ¶meter) {
QString result;
/* ... here is the expensive or blocking operation ... */
pTimer->start(interval);
emit resultReady(result);
}
private slots:
void onTimout(){
/* do something */
qDebug() << "Thread in slot is:" << QThread::currrentThread();
}
signals:
void resultReady(const QString &result);
private:
QTimer *pTimer;
};
/*---------------------------Controller----------------------------*/
class Controller : public QObject
{
Q_OBJECT
QThread workerThread;
public:
Controller() {
// 注意千万不要给创建的Worker对象指定父对象
Worker *worker = new Worker;
worker->moveToThread(&workerThread);
connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
connect(this, &Controller::operate, worker, &Worker::doWork);
connect(worker, &Worker::resultReady, this, &Controller::handleResults);
workerThread.start();
}
~Controller() {
workerThread.quit();
workerThread.wait();
}
public slots:
void handleResults(const QString &);
signals:
void operate(const QString &);
};
相较于subclass QThread
,使用worker-object
方法最突出的优势在于:
- 可以在子线程内自由地使用信号槽。在示例代码中,
Worker
类内部定义了一个定时器,响应定时器到时的槽函数onTimout()
也是运行在子线程中的。 - 可以在子线程内部定义多个槽函数,以实现不同的业务逻辑,而
subclass QThread
只能将所有任务塞进run()
函数处理。 - 可以将多个工作对象移动到一个子线程中,但需要注意的是,移动到一个线程中的工作对象是线性处理的,多个任务不能同步进行。
使用worker-object
需要注意的是:
-
QObject::moveToThread()
的作用是将槽函数放在指定的线程中调用。仅有槽函数在指定线程中调用,包括构造函数都仍然在主线程中调用!!! - 初始化
worker
对象时不要给他指定父对象。如果给work指定了父对象,就无法使用moveToThread()
(提示: QObject::moveToThread: Cannot move objects with a parent)。
2.2 线程休眠
接口 | 功能 |
void sleep(unsigned long secs) | 休眠(s) |
void msleep(unsigned long msecs) | 休眠(ms) |
void usleep(unsigned long usecs) | 休眠(us) |
2.3 正确结束线程
删除正在运行的QThread
将导致程序奔溃,需要正确地结束线程。
退出线程:quit()/exit() + wait()
// 线程的退出方式
workerThread->quit();
workerThread->wait();
如果QThread
对象在堆内存区创建:
1.有父对象:可通过finished
信号,连接deleteLater
来让线程自杀。
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
2.没有父对象:在主界面的析构函数或者通过destroyed
信号,显式地释放堆内存。
// 析构函数
~MainWindow()
{
workerThread->quit();
workerThread->wait();
workerThread->deleteLater();
}
// destroyed信号
connect(this, &MainWindow::destroyed, this, [=]()
{
workerThread->quit();
workerThread->wait();
workerThread->deleteLater();
});
3. 线程同步
3.1 互斥量
当出现多个线程需要操作同一个资源时,互斥量可用于保护一个资源一次仅被一个线程使用,Qt 中使用QMutex
或QMutexLocker
实现。
QMutex mutex;
int number = 6;
void thread1()
{
// 锁定互斥量
mutex.lock();
number *= 5;
number /= 4;
// 解锁互斥量
mutex.unlock();
}
void thread2()
{
mutex.lock();
number *= 3;
number /= 2;
mutex.unlock();
}
QMutexLocker
可以简化互斥量的处理,仅需在需要互斥量保护的函数中声明一个对象,函数结束时会自动对互斥量解锁。
void func()
{
QMutexLocker locker(&mutex);
/* do something */
}
3.2 信号量
信号量是互斥量的一般化,互斥量只能保护一个资源,而信号量可以用来保护一定数量的相同的资源,Qt 中使用QSemaphore
类实现,常用的接口为:
接口 | 功能 |
void acquire(int n = 1) | 获取n个资源(默认值1),当没有足够资源时调用者被阻塞,可用资源数量-n |
void release(int n = 1) | 释放n个资源(默认值1),可用资源数量+n |
int available() const | 返回当前可用资源的数量 |
信号量的典型用例是控制生产者/消费者之间共享的环形缓冲区,生产者/消费者对同步的需求为:
(1) 如果生产者过快地产生数据,就会覆盖消费者还没读取的数据。
(2) 如果消费者过快地读取数据,就会读取到生产者之前产生的过期数据。
可将生产者和消费者分为2个独立的线程,用2种信号量分别控制缓冲区中可写和可读的部分,可省略传统环形缓冲区中的读/写指针,简单有效。
全局变量存放在 global.h 和 global.cpp 中,使用时只需#include "global.h"
即可:
/*---------------------------global.h----------------------------*/
extern const quint32 BUFFERSIZE;
extern const quint32 DATASIZE;
extern char g_szBuffer[BUFFERSIZE];
extern QSemaphore freeBytes;
extern QSemaphore usedBytes;
/*---------------------------global.cpp----------------------------*/
#include "global.h"
const quint32 BUFFERSIZE = 20;
const quint32 DATASIZE = 40;
char g_szBuffer[BUFFERSIZE]{'\0'}; //缓冲区数组
QSemaphore freeBytes(BUFFERSIZE); //缓冲区可写资源, 初始化数量为BUFFERSIZE
QSemaphore usedBytes; //缓冲区可读资源, 初始化数量为0
生产者线程(这里采用官方推荐的worker-object方法实现):
class CProducer : public QObject
{
Q_OBJECT
public:
explicit CProducer(QObject *parent = nullptr);
public slots:
void produce();
};
void CProducer::produce()
{
while(num < DATASIZE)
{
static quint32 num = 0; //生产资源总数
quint16 len = QRandomGenerator::global()->bounded(BUFFERSIZE) + 1; //生产者一次生产的资源是随机的, 范围[1, BUFFERSIZE]
freeBytes.acquire(len);
for (auto i = num; i < num + len; ++i)
{
g_szBuffer[i % BUFFERSIZE] = QRandomGenerator::global()->bounded(100); //[i % BUFFERSIZE]是实现环形缓冲区的精髓
}
usedBytes.release(len);
num += len;
}
}
消费者线程:
class CConsumer : public QObject
{
Q_OBJECT
public:
explicit CConsumer(QObject *parent = nullptr);
public slots:
void consume();
private:
bool m_stopflag = false;
const quint16 CONSUME_DATASIZE = 20; //消费者一次消费的资源数量最大值
std::string charToHexString(const char& ch);
};
void CConsumer::consume()
{
while(num < DATASIZE)
{
static quint32 num = 0;
quint16 len = 0;
if((usedBytes.available() - CONSUME_DATASIZE) >= 0) //确保消费的资源数量不大于缓冲区内可读的资源数量
{
len = CONSUME_DATASIZE;
}
else
{
len = (usedBytes.available() == 0)? 1 : usedBytes.available(); //保证acquire的数量不为0
}
usedBytes.acquire(len);
for (auto i = num; i < num + len; ++i)
{
std::cout << charToHexString(g_szBuffer[i % BUFFERSIZE]) << std::endl;
}
freeBytes.release(len);
num += len;
}
}
main.cpp:
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
CProducer producer;
QThread producerThread;
producer.moveToThread(&producerThread);
QObject::connect(&producerThread, &QThread::started, &producer, &CProducer::produce);
CConsumer consumer;
QThread consumerThread;
consumer.moveToThread(&consumerThread);
QObject::connect(&consumerThread, &QThread::started, &consumer, &CConsumer::consume);
producerThread.start();
consumerThread.start();
producerThread.wait();
consumerThread.wait();
producerThread.quit();
consumerThread.quit();
return a.exec();
}
3.3 条件变量
条件变量用于阻塞和唤醒线程,Qt
中使用QWaitCondition
实现,常用的接口为:
接口 | 功能 |
bool wait(QMutex *lockedMutex, QDeadlineTimer deadline = QDeadlineTimer(QDeadlineTimer::Forever)) | 解锁互斥量lockedMutex并阻塞当前线程 |
void wakeOne() | 唤醒一个随机选取的被该条件变量阻塞的线程 |
void wakeAll() | 唤醒所有被该条件变量阻塞的线程 |
当调用条件变量的wait()
方法时,线程进入阻塞状态,直到该条件变量调用wakeOne()
或者wakeAll()
,被阻塞的线程才被唤醒。
使用条件变量也可以实现生产者/消费者模式:
全局变量:
/*---------------------------global.h----------------------------*/
extern const quint16 BUFFERSIZE;
extern const quint32 DATASIZE;
extern char g_szBuffer[BUFFERSIZE];
extern QWaitCondition bufferNotEmpty;
extern QWaitCondition bufferNotFull;
extern QMutex g_mutex;
extern quint16 g_usedBytesNum;
/*---------------------------global.cpp----------------------------*/
#include "global.h"
const quint16 BUFFERSIZE = 20;
const quint32 DATASIZE = 40;
char g_szBuffer[BUFFERSIZE]{'\0'}; //缓冲区数组
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex g_mutex; //使用互斥量保证原子性
quint16 g_usedBytesNum = 0; //可读资源数量
生产者线程:
class CProducer : public QObject
{
Q_OBJECT
public:
explicit CProducer(QObject *parent = nullptr);
public slots:
void produce();
};
void CProducer::produce()
{
while(num < DATASIZE)
{
static quint32 num = 0;
g_mutex.lock();
if (g_usedBytesNum == BUFFERSIZE) //缓冲区被写满时触发bufferNotFull.wait()
bufferNotFull.wait(&g_mutex);
g_mutex.unlock();
quint16 len = QRandomGenerator::global()->bounded(10);
for (auto i = num; i < num + len; ++i)
{
g_szBuffer[i % BUFFERSIZE] = QRandomGenerator::global()->bounded(100);
}
num += len;
g_mutex.lock();
g_usedBytesNum += len;
bufferNotEmpty.wakeAll(); //唤醒消费者线程
g_mutex.unlock();
}
}
消费者线程:
class CConsumer : public QObject
{
Q_OBJECT
public:
explicit CConsumer(QObject *parent = nullptr);
public slots:
void consume();
private:
bool m_stopflag = false;
const quint16 CONSUME_DATASIZE = 20; //消费者一次消费的资源数量最大值
std::string charToHexString(const char& ch);
};
void CConsumer::consume()
{
while(num < DATASIZE)
{
g_mutex.lock();
if (g_usedBytesNum == 0) //缓冲区读取完毕时触发bufferNotEmpty.wait()
bufferNotEmpty.wait(&g_mutex);
g_mutex.unlock();
static quint32 num = 0;
quint16 len = 0;
if((g_usedBytesNum - CONSUME_DATASIZE) >= 0)
{
len = CONSUME_DATASIZE;
}
else
{
len = g_usedBytesNum;
}
for (auto i = num; i < num + len; ++i)
{
std::cout << charToHexString(g_szBuffer[i % BUFFERSIZE]) << std::endl;
}
num += len;
g_mutex.lock();
g_usedBytesNum -= len;
bufferNotFull.wakeAll(); //唤醒生产者线程
g_mutex.unlock();
}
}
4. 线程池
通常情况下,一个并发的任务就会创建一个新的线程,但如果并发的线程数量很多,且线程执行时间不同步,频繁创建线程就会大大降低系统的效率,此时可以使用线程池来替我们管理线程。使用线程池有以下优点:
- 线程池可以根据系统的需求和硬件环境灵活的控制线程并发的数量,且可以对所有线程进行统一的管理和控制。
- 线程和任务分离,可以提升线程的重用性。
- 提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间。
在Qt
中使用线程池比较方便,主要用到QRunnable
和QThreadPool
类。其中QRunnable
是添加到线程池的任务类,使用时需要创建子类继承 QRunnable
,然后重写run()
方法,在这个函数内编写要执行的任务。
class CTask : public QObject, public QRunnable
{
Q_OBJECT
public:
explicit CProducer(QObject *parent = nullptr): QObject{parent}, QRunnable()
{
//任务执行完毕,该对象自动销毁
setAutoDelete(true);
}
void run() override{
/* do something */
}
};
一般情况下,在主线程中不需要创建线程池对象,直接使用QThreadPool::globalInstance()
获得线程池全局对象即可。通过调用全局对象的start()
方法就可以将一个任务添加到线程池中,这样任务就可以被线程池中的某个工作的线程处理掉了。
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 线程池初始化,设置最大线程数
QThreadPool::globalInstance()->setMaxThreadCount(4);
// 添加任务
CTask* pTask = new CTask;
QThreadPool::globalInstance()->start(pTask);
}