多线程编程是每个开发者必须掌握的基本能力之一。在上一篇文章中,我们学习了Qt多线程编程的理论知识。本文将切入实战,提供多个案例代码,帮助你彻底掌握Qt的多线程编程实践技巧。
案例1: 使用QThread执行耗时任务
这个案例演示了如何通过继承QThread和重写run()函数,在子线程中执行耗时操作,避免阻塞UI线程。
// WorkerThread.h
#include <QThread>
class WorkerThread : public QThread
{
Q_OBJECT
public:
WorkerThread() {}
protected:
void run() override {
// 耗时操作
for(int i=0; i<1000000000; ++i) {
// 一些复杂的计算
}
// 通知UI线程任务完成
emit finished();
}
signals:
void finished();
};
// main.cpp
#include <QApplication>
#include "WorkerThread.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
// 创建工作线程
WorkerThread* worker = new WorkerThread;
// 连接信号与槽
QObject::connect(worker, &WorkerThread::finished, [&](){
qDebug() << "Task finished";
});
// 启动线程
worker->start();
w.show();
return a.exec();
}
在这个例子中,我们定义了WorkerThread类继承自QThread, 并在run()函数中执行模拟的耗时操作。当耗时操作完成后,会发出finished()信号通知UI线程。在主线程中,我们创建WorkerThread实例并启动线程,连接finished()信号到一个Lambda表达式,这样就能在UI线程中安全地更新界面状态。
案例2: 使用QThreadPool执行批量任务
下面这个示例展示了如何利用QThreadPool快速并行执行大量的小任务,充分利用CPU的多核特性。
// WorkerTask.h
#include <QRunnable>
class WorkerTask : public QRunnable
{
public:
WorkerTask(int num) : m_num(num) {}
void run() override {
// 模拟耗时操作
for(int i=0; i<1000000; ++i) {
// ...
}
qDebug() << "Task" << m_num << "completed in thread" << QThread::currentThreadId();
}
private:
int m_num;
};
// main.cpp
#include <QThreadPool>
#include "WorkerTask.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 获取线程池实例
QThreadPool* pool = QThreadPool::globalInstance();
// 设置最大线程数为理想值
pool->setMaxThreadCount(QThread::idealThreadCount());
// 投递大量小任务
for(int i=0; i<1000; ++i) {
WorkerTask* task = new WorkerTask(i);
pool->start(task);
}
return a.exec();
}
在这个例子中,我们定义了WorkerTask类继承自QRunnable,它的run()函数用于模拟耗时任务。在主线程中,我们首先获取了QThreadPool的全局实例,设置最大线程数为系统理想线程数,然后循环构建1000个WorkerTask实例并通过pool->start()投递给线程池并行执行。
由于使用了线程池避免了频繁创建销毁线程的开销,所有任务能够较为高效地利用系统多核资源并行执行。当线程池中所有线程都处于繁忙状态时,新投递的任务会进入队列排队等待,所以我们要注意控制队列长度,避免性能反而下降。
案例3: 任务队列控制示例
这个示例演示了如何通过监控线程池的任务队列长度,对任务投递策略进行动态调整,防止队列过长导致性能下降。
// ThreadPoolManager.h
#include <QThreadPool>
class ThreadPoolManager : public QObject
{
Q_OBJECT
public:
static ThreadPoolManager* instance();
QThreadPool* threadPool() const { return m_threadPool; }
void enqueueTask(QRunnable* task, bool isHighPriority=false);
private:
explicit ThreadPoolManager(QObject* parent=nullptr);
~ThreadPoolManager();
QThreadPool* m_threadPool;
int m_maxThreadCount;
};
// ThreadPoolManager.cpp
#include "ThreadPoolManager.h"
ThreadPoolManager* ThreadPoolManager::instance()
{
static ThreadPoolManager manager;
return &manager;
}
ThreadPoolManager::ThreadPoolManager(QObject* parent)
: QObject(parent),
m_threadPool(QThreadPool::globalInstance()),
m_maxThreadCount(QThread::idealThreadCount())
{
m_threadPool->setMaxThreadCount(m_maxThreadCount);
}
void ThreadPoolManager::enqueueTask(QRunnable* task, bool isHighPriority)
{
// 如果队列过长,根据任务优先级调整策略
if (m_threadPool->activeThreadCount() == m_maxThreadCount &&
m_threadPool->queueLength() > 1000) {
if (isHighPriority) { // 对高优先级任务保留线程
m_threadPool->reserveThread();
} else { // 对低优先级任务延迟投递
QTimer::singleShot(50, [=]{
enqueueTask(task, false);
});
return;
}
}
// 投递任务
m_threadPool->start(task);
// 根据队列长度动态调整线程池大小
int qLen = m_threadPool->queueLength();
if (qLen > 2000) {
m_threadPool->setMaxThreadCount(m_maxThreadCount + 2);
} else if (qLen < 500) {
m_threadPool->setMaxThreadCount(m_maxThreadCount);
}
}
// 在其他地方使用
#include <QThreadPool>
#include "ThreadPoolManager.h"
WorkerTask* task = new WorkerTask;
ThreadPoolManager::instance()->enqueueTask(task, isHighPriority);
在这个例子中,我们定义了ThreadPoolManager类对QThreadPool的使用进行了封装。enqueueTask()函数在投递任务前会先检查当前线程池的状态,如果所有线程都繁忙且任务队列已经积压超过一定长度,那么就会对高优先级任务调用reserveThread()保留线程资源,对低优先级任务延迟50毫秒后重新投递。
此外,enqueueTask()还会根据任务队列的长度动态调整线程池的最大线程数,以应对突发的任务高峰。当队列长度超过2000时,最大线程数会增加;当队列长度降至500以下。
案例4: 使用QMutex保护临界区
这个例子演示了如何使用QMutex互斥锁来保护一段临界区代码,防止多个线程同时访问。
// SharedData.h
#include <QMutex>
class SharedData : public QObject
{
Q_OBJECT
public:
SharedData() : m_value(0) {}
void increase()
{
// 获取互斥锁
QMutexLocker locker(&m_mutex);
// 临界区
m_value++;
}
int value() const
{
// 由于只有读操作,无需加锁
return m_value;
}
private:
QMutex m_mutex;
int m_value;
};
// Worker.h
#include <QThread>
#include "SharedData.h"
class Worker : public QThread
{
Q_OBJECT
public:
Worker(SharedData* data) : m_data(data) {}
protected:
void run() override
{
// 每个线程执行100000次增加操作
for(int i=0; i<100000; ++i)
m_data->increase();
}
private:
SharedData* m_data;
};
// main.cpp
#include <QCoreApplication>
#include "Worker.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
SharedData data;
// 创建10个工作线程
QList<QThread*> threads;
for(int i=0; i<10; ++i)
{
Worker* worker = new Worker(&data);
worker->start();
threads.append(worker);
}
// 等待所有线程执行完毕
for(QThread* thread : threads)
thread->wait();
qDebug() << "Final value:" << data.value(); // 输出 1000000
return a.exec();
}
在这个例子中,我们定义了SharedData类,它有一个整型value成员变量和一个QMutex互斥锁m_mutex。increase()函数用于对value进行累加,在执行累加操作的临界区代码前,通过QMutexLocker自动获取互斥锁,退出临界区时自动释放锁。
Worker类继承自QThread,在run()函数中循环100000次调用SharedData::increase()函数。
在主线程中,我们创建了一个SharedData实例,然后创建10个Worker线程共享这个实例。所有线程执行完成后,我们输出SharedData的value值,由于临界区被很好地保护,最终输出的结果是正确的1000000。
如果不使用互斥锁,那么多个线程同时访问value变量,就会出现"丢失更新"的问题,导致最终结果不正确。
案例5: 使用QReadWriteLock实现读写分离
对于有读写分离需求的场景,我们可以使用QReadWriteLock来进一步提高并发性能。下面的例子演示了如何使用QReadWriteLock。
// Cache.h
#include <QReadWriteLock>
#include <QHash>
template<typename Key, typename Value>
class Cache
{
public:
bool insert(const Key& key, const Value& value)
{
QWriteLocker locker(&m_lock); // 写锁
m_cache.insert(key, value);
return true;
}
bool getValue(const Key& key, Value& value)
{
QReadLocker locker(&m_lock); // 读锁
auto it = m_cache.find(key);
if (it == m_cache.end())
return false;
value = it.value();
return true;
}
private:
QHash<Key, Value> m_cache;
QReadWriteLock m_lock;
};
// 使用示例
Cache<QString, int> cache;
cache.insert("apple", 5);
cache.insert("banana", 8);
int value;
if (cache.getValue("apple", value))
qDebug() << value; // 输出5
在这个例子中,我们定义了一个简单的键值对缓存Cache类。它内部使用QHash存储数据,使用QReadWriteLock控制数据访问。
- 写操作insert()使用QWriteLocker获取独占写锁,确保线程安全地插入或修改数据。
- 读操作getValue()只需使用QReadLocker获取共享读锁,因为只读不修改数据,所以允许多个线程同时读取。
使用读写锁的主要优势是,在存在并发读取的情况下,能够最大限度地提高并发性能。多个读线程可以同时访问共享数据而无需等待,只有存在写操作时才会阻塞所有读写线程。
这种细粒度的锁机制可以很好地应用于读多写少的场景,充分利用多核CPU的计算能力,比如数据缓存、文件缓存等领域。开发者可以根据实际需求,权衡性能与线程安全,合理选择不同的线程同步原语。
通过这几个实战案例,相信你已经对Qt的多线程编程有了更深入的理解。无论是创建工作线程执行耗时任务,还是使用线程池高效管理异步任务,亦或是用锁机制保护共享数据,Qt为我们提供了全面的多线程支持。掌握了这些技能,就能更好地开发出高性能、稳定可靠的应用程序。