Qt多线程
专题目录:
- 线程QThread
- 互斥量QMutex、QMutexLocker
- 读写锁QReadWriteLock、QReadLocker、QWriteLocker
- 信号量QSemaphore
- 等待条件QWaitCondition
- 线程数据存储器QThreadStorage
- 线程池QThreadPool
- 高级主题——QtConcurrent
一、QThread
QThread类提供了一个与平台无关的方式来管理线程。
一个QThread对象管理程序里的一个线程。当QThread对象执行run()后它管理的线程开始运行。默认情况下,run()函数通过调用
exec()函数启动该线程的事件循环。
QThread有两种用法:
- 使用QObject::moveToThread()将一个工作对象加入到某个线程中。
- 在QThread子类中重新实现run()函数。
QThread用法一:使用QObject::moveToThread()将一个工作对象加入到某个线程中
你可以通过使用QObject::moveToThread()来将一个工作对象加入到某个线程中。
代码实例:
// QThread用法一:使用QObject::moveToThread()将一个工作对象加入到某个线程中
//
// 工作对象类
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork(const QString ¶meter) {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &result);
};
// 主控制类
class Controller : public QObject
{
Q_OBJECT
QThread workerThread; // 线程对象
public:
Controller() {
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 &);
};
Worker对象的槽函数代码将在一个单独的线程中执行。然而,在任何线程内,你可以自由的地将Worker的槽函数连接来自任何对象的任何信号。跨线程的信号和槽函数连接是安全的,这归功于一种称为排队连接的实现机制。
QThread用法二:在QThread子类中重新实现run()函数
另外一种让代码在独立的线程中运行的方法,是在QThread子类中重新实现run()函数。
代码实例:
class WorkerThread : public QThread
{
Q_OBJECT
void run() override {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &s);
};
void MyObject::startWorkInAThread()
{
WorkerThread *workerThread = new WorkerThread(this); // 工作线程
connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
workerThread->start();
}
在这个例子中,线程将在run函数返回后退出。除非调用exec(), 否则线程中不会运行任何事件循环。
一定要记住,QThread实例位于实例化它的旧线程中,而不是位于调用run()的新线程中。这意味着QThread的所有队列插槽都将在旧线程中执行。因此,希望调用新线程中的插槽的开发人员必须使用worker-object方法(参考本文QThread用法一);新插槽不应该直接实现到QThread子类中。
当子类化QThread时,请记住构造函数在旧线程中执行,而run()在新线程中执行。如果这两个函数访问同一个成员变量,属于从两个不同的线程访问该变量,要检查这样的操作是否线程安全。
注意:在跨线程的对象交互时必须小心。有关详细信息,请参见同步线程(Synchronizing Threads)。
管理线程
当线程started()和finished()时,QThread将发送一个信号通知你,你或者也可以用isFinished()和isRunning()来查询线程的状态。
你可以通过调用exit()或quit()来停止线程。在极端情况下,你可能希望强制terminate()一个正在执行的线程。然而,这样做是危险的,有关详细信息,请参阅terminate()和setTerminationEnabled()的文档。
从Qt 4.8开始,通过将finished()信号连接到QObject::deleteLater(),但线程运行结束时,会自动释放线程中定义的对象。
线程调用wait()可以阻塞本身,直到其它线程完成执行(或者超过指定的时间)。
QThread还提供了与平台无关的静态睡眠函数:sleep()、msleep()和usleep(),它们分别支持完整的秒、毫秒和微秒单位级别精度。这些函数在Qt 5.0中是公开的。
注意:因为Qt是一个事件驱动的框架,wait()和sleep()函数通常是不必要的。可以考虑用finished()信号来代替wait()函数,用QTimer类替代sleep()函数。
静态函数currentThreadId()和currentThread()返回当前执行线程的标识符。前者返回值是与平台相关的线程ID;后者是一个QThread指针。
你可以在启动线程之前,通过调用setObjectName()来设置线程名(如Linux的命令ps -L显示的)。如果没有调用setObjectName(),线程默认名是线程的类名。请注意,Windows的Release版本中目前还不支持。
枚举变量
enum QThread::Priority 这个枚举变量指明了操作系统如何调度新建立的线程
二、QMutex、QMutexLocker
2.1 QMutex
QMutex类是互斥量,使得线程之间的访问可序列化。
QMutex的设计初衷是用来保护一个对象、数据结构、代码段,使得它们在同一时刻,只有一个线程有访问权限(这类似于Java synchronized 关键字)。通常最好将互斥锁与QMutexLocker一起使用,因为这样可以很容易地确保一致地执行锁定和解锁。
enum QMutex::RecursionMode (默认值为0)
枚举变量名 | 枚举值 | 描述 |
---|---|---|
QMutex::Recursive | 1 | 递归模式。在这种模式下,一个线程可以lock多次相同的互斥量,直到相应数量的unlock被调用才能被解锁。 |
QMutex::NonRecursive | 0 | 非递归模式。在这种模式下,一个线程仅可以锁互斥量一次,不可递归。 |
基本操作函数:
基本操作函数 | 说明 |
---|---|
void QMutex::lock() | 给互斥量上锁。如果互斥量被另一个线程加了锁,则阻塞直到另一个线程释放锁。如果是非递归模式的锁,重复上锁会造成死锁。 |
bool QMutex::tryLock(int timeout = 0) | 尝试给一个互斥量加锁,如果这个互斥量没被锁,则返回true,如果被锁,则等待timeout时间,等待其他线程释放锁,当timeout为负数时,一直等待。 |
void QMutex::unlock() | 给mutex解锁,给一个未lock的mutex解锁,将有不可预知的结果,尝试去给不同的线程中的mutex解锁,会导致错误。 |
bool QMutex::isRecursive() const | 如果mutex为递归,则返回true。 |
代码实例:
说明:QMutex变量可以和临界资源定义在一起,每次线程访问前都需要对互斥量进行锁定,然后进行操作,操作完后再释放互斥量。
// QMutex示例
QMutex mutex;
int number = 6;
// A线程中的方法
void method1()
{
mutex.lock(); // 上锁
number *= 5;
number /= 4;
mutex.unlock(); // 解锁
}
// B线程中的方法
void method2()
{
mutex.lock();
number *= 3;
number /= 2;
mutex.unlock();
}
2.2 QMutexLocker
QMutexLocker是一个用来简化互斥量锁定和解锁操作的类。
在复杂函数或者异常处理代码中互斥量的锁定和解锁容易出错和难以调试。QMutexLocker就可以应用于这些情况,确保互斥量状态总是定义明确。
应该在程序中QMutex需要被锁定处创建QMutexLocker。当QMutexLocker被创建后,互斥量就锁定了。你可以使用unlock()和relock()来解锁和重锁互斥量。如果互斥量被锁定,当QMutexLocker销毁的时候,会自动解锁互斥量。
例如,在以下这个复杂函数中,进入函数开始处锁定互斥量,在各个退出点解锁互斥量:
// 复杂函数中用QMutex的例子
int complexFunction(int flag)
{
mutex.lock();
int retVal = 0;
switch (flag) {
case 0:
case 1:
retVal = moreComplexFunction(flag);
break;
case 2:
{
int status = anotherFunction();
if (status < 0) {
mutex.unlock();
return -2;
}
retVal = status + flag;
}
break;
default:
if (flag > 10) {
mutex.unlock();
return -1;
}
break;
}
mutex.unlock();
return retVal;
}
当这个示例函数变得更复杂的时候,将增加出错的可能性。
使用QMutexLocker大大地简化代码,增加代码可读性:
// 复杂函数中用QMutexLocker的例子
int complexFunction(int flag)
{
QMutexLocker locker(&mutex);
int retVal = 0;
switch (flag) {
case 0:
case 1:
return moreComplexFunction(flag);
case 2:
{
int status = anotherFunction();
if (status < 0)
return -2;
retVal = status + flag;
}
break;
default:
if (flag > 10)
return -1;
break;
}
return retVal;
}
现在,当QMutexLocker对象被销毁的时候,就自动实现互斥量的解锁(因为QMutexLocker是个局部变量,当函数返回时它就被销毁)。
这原则同样适用于抛出和捕获异常的代码。在异常处理函数中,如果互斥量已经被锁定,但是在异常被抛出之前还没有其他的地方解锁互斥量,那么异常将不被捕获。
QMutexLocker也提供了一个mutex()成员函数返回QMutexLocker操作的互斥量。对于需要访问互斥量是十分有用的,比如QWaitCondition::wait()。
例如:
// QMutexLocker::mutex()用法示例
class SignalWaiter
{
private:
QMutexLocker locker;
public:
SignalWaiter(QMutex *mutex)
: locker(mutex)
{
}
void waitForSignal()
{
...
while (!signalled)
waitCondition.wait(locker.mutex());
...
}
};
基本操作函数:
基本操作函数 | 说明 |
---|---|
QMutexLocker(QMutex *mutex) | 构造一个QMutexLocker对象并锁住互斥量。当QMutexLocker被销毁的时候,互斥量将被自动解锁。如果互斥量为0,QMutexLocker不起作用。 |
~QMutexLocker () | 销毁QMutexLocker并且解锁在它构造函数中被锁定互斥量 |
QMutexLocker::mutex () | 返回在QMutexLocker构造函数中被锁定的互斥量 |
reloack() | 锁定一个解锁状态的互斥量 |
unlock() | 解锁互斥量。 |
三、QReadWriteLock、QReadLocker、QWriteLocker
3.1 QReadWriteLock
Qt中的QReadWriteLock类为我们提供了读写锁的功能。
读写锁是用来保护可以被读访问和写访问的资源的一种同步工具。如果你想让多个线程同时的对资源进行读访问,但只要有一个线程要对资源进行写访问时,所有其他的线程必须等待,直到写访问完成。对于这种情况,读写锁是非常有用的。
在很多情况下,QReadWriteLock是QMutex的直接竞争对手。如果有很多并发的读操作,很少的写操作时,QReadWriteLock是一个很好的选择。
代码实例:
// QReadWriteLock(读写锁)的使用示例
QReadWriteLock lock;
void ReaderThread::run()
{
...
lock.lockForRead();
read_file();
lock.unlock();
...
}
void WriterThread::run()
{
...
lock.lockForWrite();
write_file();
lock.unlock();
...
}
为了确保写操作不会被读操作永远阻塞,当有阻塞的写操作在等待时,请求读操作会被阻塞,即使当前的锁由另一个读操作持有。同样,当锁被一个写操作持有时,另一个写操作又进来了,那么后来的写操作将会优先于所有的读操作而先获得锁。
和QMutex一样,QReadWriteLock也可以被同一个线程递归的锁定,只要在构造函数中传入一个QReadWriteLock::Recursive标志。在这种情况下,unlock()被调用的次数必须和lockForWrite()或lockForRead()被调用的次数一样多。但要注意,当递归锁定时,锁类型不能改变,也就是说,在已经锁定为写的线程中无法锁定为读(反之亦然)。(即一个线程中只能有读写中的一种操作,只能加一种锁,不允许一个线程对一个文件同时支持读写操作)。
同样,为了方便使用QReadWriteLock,Qt还提供了QReadLocker和QWriteLocker两个方便类。能使我们更方便的使用读写锁的功能。
enum QReadWriteLock::RecursionMode (默认值为0)
枚举变量名 | 枚举值 | 描述 |
---|---|---|
QReadWriteLock::Recursive | 1 | 递归模式。在这种模式下,一个线程可以lock多次相同的读写锁,直到相应数量的unlock()被调用才能被解锁。 |
QReadWriteLock::NonRecursive | 0 | 非递归模式。在这种模式下,一个线程仅可以锁读写锁一次,不可递归。 |
基本操作函数:
基本操作函数 | 说明 |
---|---|
QReadWriteLock::QReadWriteLock(RecursionMode recursionMode = NonRecursive) | 用给定的recursionMode参数构造一个读写锁。 |
QReadWriteLock::~QReadWriteLock() | 销毁一个读写锁。注意,当销毁一个正在使用的读写锁时可能引起未知错误。 |
lockForRead() | 读锁定。如果其他的线程已经写锁定了,则该函数的读操作会阻塞。 |
lockForWrite() | 写锁定。如果其他的线程已经读锁定或者写锁定了,则该函数的写操作会阻塞。(除非创建的是递归锁类型) |
bool QReadWriteLock::tryLockForRead() | 尝试读锁定。如果读锁定获得成功,返回true,否则返回false,它不会阻塞。 |
bool QReadWriteLock::tryLockForRead(int timeout) | 尝试读锁定。如果读锁定成功,则返回true,如果获得不成功,则等待timeout时间,等待其他线程释放锁,当timeout为负数时,一直等待。 |
bool QReadWriteLock::tryLockForWrite() | 尝试写锁定。如果写锁定成功,则返回true,否则它立即返回false。 |
bool QReadWriteLock::tryLockForWrite(int timeout) | 尝试写锁定。如果写锁定成功,则返回true,如果获得不成功,则等待timeout时间,等待其他线程释放锁,当timeout为负数时,一直等待。 |
void QReadWriteLock::unlock() | 释放锁。释放一个未锁定的锁是错误的,会引起程序中断。 |
3.2 QReadLocker
QReadLocker是一个用来简化读写锁中读操作的上锁和解锁操作的类。
QReadLocker(和QWriteLocker)的目的是简化QReadWriteLock的上锁和解锁。异常处理代码中读写锁的锁定和解锁容易出错和难以调试。QReadLocker就可以应用于这些情况,确保读写锁状态总是定义明确。
代码实例:
// 1.QReadLocker的使用示例
QReadWriteLock lock;
QByteArray readData()
{
QReadLocker locker(&lock);
...
return data;
}
// 2.QReadWriteLock实现代码(与上等价)
QReadWriteLock lock;
QByteArray readData()
{
lock.lockForRead();
...
lock.unlock();
return data;
}
基本操作函数:
基本操作函数 | 说明 |
---|---|
QReadLocker(QReadWriteLock *) | 创建一个QReadLocker,并且读锁定。 |
~QReadLocker() | 销毁QReadLocker,并且释放读锁定。 |
readWriteLock() const : QReadWriteLock * | 获得读写锁的指针。 |
relock() | 对未锁定的读写锁重新上读锁。 |
unlock() | 释放锁 |
3.3 QWriteLocker
QWriteLocker是一个用来简化读写锁中写操作的上锁和解锁操作的类。
QWriteLocker(和QReadLocker)的目的是简化QReadWriteLock的上锁和解锁。异常处理代码中读写锁的锁定和解锁容易出错和难以调试。QWriteLocker就可以应用于这些情况,确保读写锁状态总是定义明确。
代码实例:
// 1.QWriteLocker的使用示例
QReadWriteLock lock;
void writeData(const QByteArray &data)
{
QWriteLocker locker(&lock);
...
}
// 2.QReadWriteLock实现代码(与上等价)
QReadWriteLock lock;
void writeData(const QByteArray &data)
{
lock.lockForWrite();
...
lock.unlock();
}
基本操作函数:
基本操作函数 | 说明 |
---|---|
QWriteLocker(QReadWriteLock *) | 创建一个QWriteLocker,并且写锁定。 |
~QWriteLocker() | 销毁QWriteLocker,并且释放写锁定。 |
**readWriteLock() const | QReadWriteLock ***: 获得读写锁的指针。 |
relock() | 对未锁定的读写锁重新上写锁。 |
unlock() | 释放锁。 |
四、QSemaphore
QSemaphore类是计数信号量,它是对互斥量(QMutex)功能的一种扩展。互斥量只能锁定一次,而信号量可以获取多次,信号量通常用于保护一定数量的同种资源。
信号量支持两个基本操作:acquire()和release()。
基本操作函数:
基本操作函数 | 说明 |
---|---|
acquire(n) | 函数用于获取n个资源。如果没有足够多可用资源时,调用者将被阻塞直到有足够的可用资源。 |
release(n) | 函数用于释放n个资源 |
tryAcquire() | 函数用于获取资源,如果它不能获取资源,会立即返回false。 |
available() | 函数可在任何时候返回可用资源的数量。 |
信号量的典型用例就是 生产者和消费者问题。控制生产者/消费者之间共享的环形缓冲区。
比如在餐馆里用餐,一个信号量由餐厅里的椅子数量初始化。到达餐馆就餐的人都想要一个座位。当座位越来越满,available()在减少;当人们离开,available()增加,允许更多的人坐下来就餐。
代码实例:
// 生产者与消费者问题(信号量实现)
#include <QtCore>
#include <stdio.h>
#include <stdlib.h>
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QSemaphore freeBytes(BufferSize); // 可被生产者填充的缓冲区部分
QSemaphore usedBytes; // 可被消费者读取的缓冲区部分
// 生产者线程
class Producer : public QThread
{
public:
void run() override
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for (int i = 0; i < DataSize; ++i) {
freeBytes.acquire(); // 获取一个空闲单元
buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
usedBytes.release(); // 释放一个消费者可用单元
}
}
};
// 消费者线程
class Consumer : public QThread
{
Q_OBJECT
public:
void run() override
{
for (int i = 0; i < DataSize; ++i) {
usedBytes.acquire(); // 获取一个消费者可读单元
fprintf(stderr, "%c", buffer[i % BufferSize]);
freeBytes.release(); // 释放一个空闲单元
}
fprintf(stderr, "\n");
}
}
五、QWaitCondition
QWaitCondition类提供一个条件变量来实现线程间同步。
QWaitConditions允许一个线程告诉其它线程某种条件已经满足,一个或多个线程可以等待一个由wakeOne()或wakeAll()设定的条件QWaitCondition。使用wakeOne()会唤醒随机选择的一个线程,使用wakeAll()会唤醒所以线程。
比如,假定每次用户按下一个键,我们有三个任务要同时执行,每个任务都可以放到一个线程中,每个线程的run()都应该是这样:
forever {
mutex.lock();
keyPressed.wait(&mutex);
do_something();
mutex.unlock();
}
这里,keyPressed变量是个全局的QWaitCondition.类型变量。
第四个线程应该读按键,每当它收到一个按键就唤醒其他三个线程,它应该像这样:
forever {
getchar();
keyPressed.wakeAll();
}
注意这三个线程被唤醒的顺序是未定义的,并且当键被按下时,这些线程中的一个或多个还在do_something(),它们将不会被唤醒(因为它们现在没有等待条件变量)并且这个任务也就不会针对这次按键执行操作。这种情况是可以通过使用一个计数器和互斥量来避免,比如,修改后的线程代码如下:
forever {
mutex.lock();
keyPressed.wait(&mutex);
++count;
mutex.unlock();
do_something();
mutex.lock();
--count;
mutex.unlock();
}
这是第四个线程的代码:
forever {
getchar();
mutex.lock();
// Sleep until there are no busy worker threads
while (count > 0) {
mutex.unlock();
sleep(1);
mutex.lock();
}
keyPressed.wakeAll();
mutex.unlock();
}
互斥量是必须的,因为两个线程试图同时对同一个变量进行修改的结果是不可预知的。
基本操作函数:
基本操作函数 | 说明 |
---|---|
QWaitCondition() | 创建一个新的等待条件对象 |
~QWaitCondition() | 销毁一个等待条件对象 |
bool wait(QMutex *lockedMutex, unsigned long time = ULONG_MAX) | 释放锁定的lockedMutex并且在线程事件对象上等待。lockedMutex必须由调用线程初始锁定的。如果lockedMutex没有在锁定状态,这个函数的行为是未定义的。如果lockedMutex是一个递归互斥量,这个函数立即返回。 lockedMutex将被解锁,并且调用线程将会阻塞,直到下列条件之一满足时才醒来: 1.另一个线程使用wakeOne()或wakeAll()传输信号给它。在这种情况下,函数将返回true。 2. time毫秒过去了。如果time为ULONG_MAX(默认值),那么这个等待将永远不会超时(这个事件必须被传输)。如果等待的事件超时,这个函数将会返回false。 lockedMutex将以同样的锁定状态返回。这个函数提供的是允许从锁定状态到等待状态的原子转换。 |
bool wait(QReadWriteLock *lockedReadWriteLock, unsigned long time = ULONG_MAX) | 释放lockedReadWriteLock,等待条件变量。lockedReadWriteLock必须由调用线程初始锁定的。如果lockedReadWriteLock没有在锁定状态,这个函数立即返回。lockedReadWriteLock必须保证没有被递归锁定,否则函数将无法正确释放锁。 lockedReadWriteLock将被解锁,并且调用线程将会阻塞,直到下列条件之一满足时才唤醒: 1. 另一个线程使用wakeOne()或wakeAll()传输信号给它。在这种情况下,这个函数将返回true。 2. time毫秒过去了。如果time为ULONG_MAX(默认值),那么这个等待将永远不会超时(这个事件必须被传输)。如果等待的事件超时,这个函数将会返回false。 互斥量将以同样的锁定状态返回。这个函数提供的是允许从锁定状态到等待状态的原子转换。 |
void wakeAll() | 唤醒所有等待QWaitCondition的线程。这些线程被唤醒的顺序依赖于操作系统的调度策略,并且不能被控制或预知。 |
void wakeOne() | 唤醒某个等待QWaitCondition的线程。具体是那个线程被唤醒取决于操作系统的调度策略,并且不能被控制或预知。 如果想唤醒某个指定的线程,一般的办法是定义不同的等待条件QWaitCondition,让不同的线程等待不同的等待条件。 |
等待条件是一个有用的线程同步方法。等待条件的例子展示了如何运用QWaitCondition代替QSemaphore来实现操作环形缓冲区。消费者和生产者共享环形缓冲区。
代码实例:
// 生产者与消费者问题(条件变量实现)
#include <QtCore>
#include <stdio.h>
#include <stdlib.h>
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize]; // 缓冲区大小
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;
// 生产者线程
class Producer : public QThread
{
public:
Producer(QObject *parent = NULL) : QThread(parent)
{
}
void run() override
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == BufferSize)
bufferNotFull.wait(&mutex);
mutex.unlock();
buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
mutex.lock();
++numUsedBytes;
bufferNotEmpty.wakeAll();
mutex.unlock();
}
}
};
// 消费者线程
class Consumer : public QThread
{
Q_OBJECT
public:
Consumer(QObject *parent = NULL) : QThread(parent)
{
}
void run() override
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == 0)
bufferNotEmpty.wait(&mutex);
mutex.unlock();
fprintf(stderr, "%c", buffer[i % BufferSize]);
mutex.lock();
--numUsedBytes;
bufferNotFull.wakeAll();
mutex.unlock();
}
fprintf(stderr, "\n");
}
signals:
void stringConsumed(const QString &text);
};
// 主程序
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}
六、QThreadStorage
QThreadStorage是一个模板类,提供按线程存储数据,线程数据互不干扰。
setLocalData()函数:为调用线程存储一个特定于线程的数据。稍后可以通过 localData() 来访问这些数据。
hasLocalData()函数:程序员用它来判断之前是否用setLocalData()函数设置过数据。这对于延迟初始化也很有用。
如果T是指针类型,那么QThreadStorage将获得数据的所有权(必须是用new在堆上创建的数据),并在线程退出时删除它,无论是正常退出还是终止退出。
以下例子展示了使用QThreadStorage来为每个线程存储一份缓存,它通过调用cacheObject()和removeFromCache()函数来插入和删除。当线程退出时,缓存也会随之自动删除。
QThreadStorage<QCache<QString, SomeClass> > caches;
void cacheObject(const QString &key, SomeClass *object)
{
caches.localData().insert(key, object);
}
void removeFromCache(const QString &key)
{
if (!caches.hasLocalData())
return;
caches.localData().remove(key);
}
特别值得注意的是:
QThreadStorage析构函数不会删除每个线程的数据。QThreadStorage只在线程退出或多次调用setLocalData()时删除每个线程的数据。
QThreadStorage可用于存储main()线程的数据。当QApplication被销毁时,QThreadStorage删除main()线程的所有数据集,不管main()线程是否已经结束。
七、QThreadPool
QThreadPool类管理一个线程集合。
QThreadPool管理和回收各个QThread对象,从而让程序降低线程的创建成本。每个Qt应用程序都有一个全局的QThreadPool对象,可以通过调用globalInstance()来访问它。
要使用一个QThreadPool线程,可以子类化QRunnable并实现run()虚函数。然后创建该类的对象,并将其传递给QThreadPool::start()。
// QThreadPool用法(Qt线程用法三)
class HelloWorldTask : public QRunnable
{
void run()
{
qDebug() << "Hello world from thread" << QThread::currentThread();
}
};
HelloWorldTask *hello = new HelloWorldTask();
// QThreadPool takes ownership and deletes 'hello' automatically
QThreadPool::globalInstance()->start(hello);
QThreadPool默认情况下会自动删除QRunnable对象。 调用QRunnable::setAutoDelete()可以改变自动删除标志。
QThreadPool支持用tryStart(this)多次执行同一个QRunnable的 QRunnable::run()函数。设置autoDelete为true时,从QRunnable的run()最终退出时会删除这个QRunable。在设置autoDelete时,多次调用同一个QRunnable的start()会形成一个竞态,不建议这样做。
线程在一段时间内未使用将过期。默认过期时间为30000毫秒(30秒)。setExpiryTimeout()可以修改它,当参数是负数时,过期时间为无穷大。
maxThreadCount()能用来查询支持的最大线程数。也可以使用setMaxThreadCount()来更改支持的最大线程数。默认的maxThreadCount()是QThread::idealThreadCount()。activeThreadCount()函数返回当前正在工作的线程数。
reserveThread()函数为外部使用保留一个线程。完成线程时使用releaseThread(),以便可以重用它。本质上,这些函数会暂时增加或减少活动线程数,并且在实现QThreadPool不可见的耗时操作时非常有用。
请注意,QThreadPool是一个用于管理线程的low-level类,高级别用法请参阅Qt Concurrent模块。
基本操作函数 | 说明 |
---|---|
QThreadPool *QThreadPool::globalInstance() | 返回一个全局QThreadPool实例指针。 |
activeThreadCount : const int | 返回线程池中的活动线程数 |
void QThreadPool::clear() | 移除队列中还未启动的runnables线程。 |
int expiryTimeout() const | 过期时间 |
int maxThreadCount() const | 线程池可用的最大线程数。 |
void releaseThread() | 释放之前通过调用reserveThread()保留的线程。 |
void QThreadPool::reserveThread() | 保留一个线程 |
void setExpiryTimeout(int expiryTimeout) | 设置过期时间 |
setMaxThreadCount(int maxThreadCount) | 设置可用最大线程数 |
void QThreadPool::start(QRunnable *runnable, int priority = 0) | 保留一个线程,并用它来运行runnable,除非超过maxThreadCount()。在这种情况下,runnable被添加到run队列中。priority参数可用于控制run队列的执行顺序. |
bool tryStart(QRunnable *runnable) | 试图保留一个线程去启动runnable。 |
bool tryTake(QRunnable *runnable) | 如果指定的runnable尚未启动,则尝试从队列中删除它。如果尚未启动runnable,则返回true, runnable的所有权转移给调用者(即runnable->autoDelete() == true)。否则返回false。 |
bool waitForDone(int msecs = -1) | 等待所有线程退出,并从线程池中删除所有线程,最长可达msecs毫秒。如果所有线程都被删除,则返回true;否则返回false。如果msecs为-1(默认值),则忽略超时(等待最后一个线程退出)。 |
八、高级主题
8.1 QtConcurrent
QtConcurrent命名空间提供了 high-level api,可以在不使用low-level线程原语的情况下编写多线程程序。
QtConcurrent::run()在一个单独的线程中运行一个函数。函数的返回值通过QFuture API提供。
1. 全局函数或静态函数,作为线程函数
用QtConcurrent::run(),使一个函数运行在一个独立的线程中:
// 运行在全局的线程池中
extern void aFunction();
QFuture<void> future = QtConcurrent::run(aFunction);
// 运行在指定的线程池中
extern void aFunction();
QThreadPool pool;
QFuture<void> future = QtConcurrent::run(&pool, aFunction);
// 函数传参示例
extern void aFunctionWithArguments(int arg1, double arg2, const QString &string);
int integer = ...;
double floatingPoint = ...;
QString string = ...;
QFuture<void> future = QtConcurrent::run(aFunctionWithArguments, integer, floatingPoint, string);
// 函数获得返回值示例
extern QString functionReturningAString();
QFuture<QString> future = QtConcurrent::run(functionReturningAString);
...
QString result = future.result();
// 函数传参、获得返回值示例
extern QString someFunction(const QByteArray &input);
QByteArray bytearray = ...;
QFuture<QString> future = QtConcurrent::run(someFunction, bytearray);
...
QString result = future.result();
2. 类成员函数作为线程函数
// call 'QList<QByteArray> QByteArray::split(char sep) const' in a separate thread
QByteArray bytearray = "hello world";
QFuture<QList<QByteArray> > future = QtConcurrent::run(bytearray, &QByteArray::split, ',');
...
QList<QByteArray> result = future.result();
// call 'void QImage::invertPixels(InvertMode mode)' in a separate thread
QImage image = ...;
QFuture<void> future = QtConcurrent::run(&image, &QImage::invertPixels, QImage::InvertRgba);
...
future.waitForFinished();
// At this point, the pixels in 'image' have been inverted
3. Lambda函数作为线程函数
QFuture<void> future = QtConcurrent::run([=]() {
// Code in this block will run in another thread
});
...
Qt多线程的更多详细介绍,请参考Qt Assistant的索引Thread Support in Qt。