在高并发的软件系统中,多线程编程是解决性能瓶颈和提高系统吞吐量的有效手段。作为跨平台的应用程序开发框架,Qt为我们提供了强大的多线程支持。本文将深入探讨Qt多线程编程的实现细节,并介绍线程池的设计思想,帮助读者彻底掌握Qt多线程编程技巧。
Qt中实现多线程编程主要有两种方式:重写QThread类的run()函数和使用信号与槽。
这种方式需要继承QThread类并重写虚函数run(),将需要并发执行的代码逻辑放在run()函数中。例如:
class WorkThread : public QThread { public: void run() override { //并发执行的代码 qDebug() << "Current thread:" << QThread::currentThreadId(); //执行耗时操作 heavyWorkLoad(); } };
在主线程中,我们只需创建WorkThread对象并调用start()即可启动新线程:
WorkThread *worker = new WorkThread; worker->start();
这种方法的优点是直观简单,缺点是run()函数作为线程执行体只能有一个入口,不太适合处理多个工作单元并发执行的场景。
Qt的信号与槽机制也可以用于实现多线程编程,它的思路是:
(1)、创建QThread对象作为新线程
(2)、创建执行体对象,并使用QObject::moveToThread()将其移动到新线程
(3)、在主线程通过连接信号与槽的方式,间接调用执行体对象的槽函数,从而启动新线程中的任务
具体代码如下:
//ExecutionBody.h class ExecutionBody : public QObject { Q_OBJECT public slots: void execution() { //并发执行的代码 qDebug() << "Executing in thread" << QThread::currentThreadId(); heavyWorkLoad(); } }; //main.cpp int main() { QThread *worker = new QThread; ExecutionBody *body = new ExecutionBody; body->moveToThread(worker); QObject::connect(worker, &QThread::started, body, &ExecutionBody::execution); worker->start(); return app.exec(); }
相比靠前种方法,信号与槽方式支持在新线程中执行多个函数,更加灵活。但也相对复杂一些,开发者需要清晰地理解信号连接、事件循环等概念。
前面介绍了Qt的基本多线程实现方式,不过在实际项目中,如果只是简单地启动固定数量的线程,可能会面临以下问题:
(1)、线程的创建和销毁代价较高
(2)、线程数量太多,会加重系统的线程调度开销
(3)、大量线程空转,造成CPU资源浪费
为了解决这些问题,我们需要引入线程池的概念,将闲置的线程资源统一管理和调度,避免频繁创建和销毁线程。Qt提供了QThreadPool类实现了这一机制。
QThreadPool内部管理了一组工作线程(工作者线程),当有任务投递时,线程池会将任务分配给空闲的工作线程执行,避免频繁创建和销毁线程。此外,QThreadPool还支持设置活跃线程数上限,在线程全部忙碌时也不会盲目创建新的工作线程,从而避免过度占用系统资源。
QThreadPool采用信号与槽的方式将任务分发给工作线程。具体来说,当我们调用QThreadPool::start()投递任务时,QThreadPool会为任务创建一个QRunnable对象,并通过内部信号连接到某个工作线程,由工作线程执行QRunnable的run()函数。
下面通过一个简单的例子展示如何使用QThreadPool:
//WorkerTask.h class WorkerTask : public QRunnable { public: void run() override { //执行任务逻辑 qDebug() << "Executing task in thread" << QThread::currentThreadId(); heavyWorkLoad(); } }; //main.cpp int main() { QThreadPool *pool = QThreadPool::globalInstance(); //设置最大线程数 pool->setMaxThreadCount(QThread::idealThreadCount()); //投递任务 for(int i=0; i<200; ++i) { WorkerTask *task = new WorkerTask; pool->start(task); } return app.exec(); }
这个示例首先获取全局QThreadPool实例,并设置最大工作线程数为当前系统的理想线程数(通常为CPU核心数)。然后循环构建WorkerTask对象并调用QThreadPool::start()投递,线程池会自动将任务分发给空闲的线程执行。
需要注意的是,QThreadPool默认采用栈内存管理QRunnable对象,也就是说在QRunnable的run()函数执行完毕后,QThreadPool会自动销毁对象。如果我们需要在run()函数执行完毕后继续访问QRunnable对象的数据成员,应该设置QThreadPool的stackSize属性(即将对象放在堆内存分配)。
尽管QThreadPool大大简化了多线程编程流程,但在实际开发中,我们仍需注意一些潜在的安全隐患和性能风险:
当多个线程并发访问同一份数据时,很容易出现竞态条件。Qt提供了QMutex、QSemaphore、QReadWriteLock等同步原语类,我们可以利用它们来保护线程间共享数据的完整性。
另外,Qt还提供了QAtomicInteger和QAtomicPointer等原子操作类,能够确保基础数据类型的读写操作的原子性。对于简单的计数、状态位的读写,使用原子操作类可以避免加锁开销。
使用QThreadPool虽然能避免频繁创建销毁线程,但如果任务投递过多且执行时间过长,任务队列会持续积压,可能导致响应延迟或内存占用過高。
因此,我们需要对任务队列的长度作出合理控制。QThreadPool提供了两个相关的API:
我们可以在投递任务前检查当前队列长度,对于优先级较高的任务使用reserveThread()保留资源,对于优先级较低的任务可以选择延迟投递或动态增加线程池大小。
在多线程编程中,如果多个线程互相持有对方所需要的锁资源,就会发生死锁。例如下面的代码:
QMutex mutex1, mutex2; //线程1 mutex1.lock(); ... mutex2.lock(); //阻塞 //线程2 mutex2.lock(); ... mutex1.lock(); //阻塞
避免死锁的一个常用策略是:对所有需要加锁的代码采用统一的加锁顺序,每个线程按相同顺序申请锁。
线程切换是一个非常耗时的操作,会带来较大的性能开销。我们应该尽量减少线程切换的发生,例如:
通过本文的介绍,希望你能够加深对Qt多线程编程的理解,在实际开发中合理使用多线程,提高应用程序的整体性能。下一篇文章将为你带来更多实战案例,进一步展示Qt多线程编程的实践技巧。
TAG:qt多线程