Qt5.14.2 深入理解Qt多线程编程,掌握线程池架构实现高效并发 ...

打印 上一主题 下一主题

主题 998|帖子 998|积分 2994

在高并发的软件体系中,多线程编程是解决性能瓶颈和提高体系吞吐量的有效手段。作为跨平台的应用程序开发框架,Qt为我们提供了强大的多线程支持。本文将深入探究Qt多线程编程的实现细节,并先容线程池的计划思想,资助读者彻底掌握Qt多线程编程本领。

一、Qt的两种多线程实现方式分析

Qt中实现多线程编程主要有两种方式:重写QThread类的run()函数和使用信号与槽。

1、重写QThread的run()函数

这种方式需要继续QThread类并重写虚函数run(),将需要并发执行的代码逻辑放在run()函数中。例如:
  1. class WorkThread : public QThread {
  2. public:
  3.     void run() override {
  4.         //并发执行的代码
  5.         qDebug() << "Current thread:" << QThread::currentThreadId();
  6.         //执行耗时操作
  7.         heavyWorkLoad();
  8.     }
  9. };
复制代码

在主线程中,我们只需创建WorkThread对象并调用start()即可启动新线程:
  1. WorkThread *worker = new WorkThread;
  2. worker->start();
复制代码

这种方法的长处是直观简单,缺点是run()函数作为线程执行体只能有一个入口,不太得当处置惩罚多个工作单元并发执行的场景。

2、使用信号与槽方式

Qt的信号与槽机制也可以用于实现多线程编程,它的思绪是:
(1)、创建QThread对象作为新线程
(2)、创建执行体对象,并使用QObject::moveToThread()将其移动到新线程
(3)、在主线程通过连接信号与槽的方式,间接调用执行体对象的槽函数,从而启动新线程中的使命


具体代码如下:
  1. //ExecutionBody.h
  2. class ExecutionBody : public QObject {
  3.     Q_OBJECT
  4. public slots:
  5.     void execution() {
  6.         //并发执行的代码  
  7.         qDebug() << "Executing in thread" << QThread::currentThreadId();
  8.         heavyWorkLoad();
  9.     }
  10. };
  11. //main.cpp
  12. int main() {
  13.     QThread *worker = new QThread;
  14.     ExecutionBody *body = new ExecutionBody;
  15.     body->moveToThread(worker);
  16.    
  17.     QObject::connect(worker, &QThread::started, body, &ExecutionBody::execution);
  18.     worker->start();
  19.    
  20.     return app.exec();
  21. }
复制代码
相比第一种方法,信号与槽方式支持在新线程中执行多个函数,更加灵活。但也相对复杂一些,开发者需要清晰地理解信号连接、变乱循环等概念。

二、突破瓶颈,构建高效线程池

前面先容了Qt的基本多线程实现方式,不外在实际项目中,如果只是简单地启动固定数量的线程,可能会面临以下问题:
(1)、线程的创建和销毁代价较高
(2)、线程数量太多,会加重体系的线程调度开销
(3)、大量线程空转,造成CPU资源浪费
为了解决这些问题,我们需要引入线程池的概念,将闲置的线程资源统一管理和调度,避免频繁创建和销毁线程。Qt提供了QThreadPool类实现了这一机制。

1、QThreadPool计划原理


QThreadPool内部管理了一组工作线程(工作者线程),当有使命投递时,线程池会将使命分配给空闲的工作线程执行,避免频繁创建和销毁线程。别的,QThreadPool还支持设置活跃线程数上限,在线程全部忙碌时也不会盲目创建新的工作线程,从而避免过分占用体系资源。


QThreadPool采用信号与槽的方式将使命分发给工作线程。具体来说,当我们调用QThreadPool::start()投递使命时,QThreadPool会为使命创建一个QRunnable对象,并通过内部信号连接到某个工作线程,由工作线程执行QRunnable的run()函数。




2、QThreadPool使用示例

下面通过一个简单的例子展示怎样使用QThreadPool:
  1. //WorkerTask.h
  2. class WorkerTask : public QRunnable {
  3. public:
  4.     void run() override {
  5.         //执行任务逻辑
  6.         qDebug() << "Executing task in thread" << QThread::currentThreadId();
  7.         heavyWorkLoad();
  8.     }
  9. };
  10. //main.cpp
  11. int main() {
  12.     QThreadPool *pool = QThreadPool::globalInstance();
  13.    
  14.     //设置最大线程数
  15.     pool->setMaxThreadCount(QThread::idealThreadCount());
  16.    
  17.     //投递任务
  18.     for(int i=0; i<200; ++i) {  
  19.         WorkerTask *task = new WorkerTask;
  20.         pool->start(task);
  21.     }
  22.     return app.exec();
  23. }
复制代码

这个示例起首获取全局QThreadPool实例,并设置最大工作线程数为当前体系的理想线程数(通常为CPU核心数)。然后循环构建WorkerTask对象并调用QThreadPool::start()投递,线程池会主动将使命分发给空闲的线程执行。


需要注意的是,QThreadPool默认采用栈内存管理QRunnable对象,也就是说在QRunnable的run()函数执行完毕后,QThreadPool会主动销毁对象。如果我们需要在run()函数执行完毕后继续访问QRunnable对象的数据成员,应该设置QThreadPool的stackSize属性(即将对象放在堆内存分配)。

三、多线程开发中的注意事项

尽管QThreadPool大大简化了多线程编程流程,但在实际开发中,我们仍需注意一些埋伏的安全隐患和性能风险:


1、线程间数据访问安全

当多个线程并发访问同一份数据时,很轻易出现竞态条件。Qt提供了QMutex、QSemaphore、QReadWriteLock等同步原语类,我们可以使用它们来保护线程间共享数据的完整性。


别的,Qt还提供了QAtomicInteger和QAtomicPointer等原子操纵类,可以大概确保底子数据类型的读写操纵的原子性。对于简单的计数、状态位的读写,使用原子操纵类可以避免加锁开销。

2、使命队列控制战略

使用QThreadPool虽然能避免频繁创建销毁线程,但如果使命投递过多且执行时间过长,使命队列会一连积压,可能导致相应延迟或内存占用過高。
因此,我们需要对使命队列的长度作出合理控制。QThreadPool提供了两个相干的API:


  • QThreadPool::reserveThread()可以为高优先级使命预留线程资源
  • QThreadPool::setMaxThreadCount()可以动态调整线程池的最大线程数
我们可以在投递使命前检查当前队列长度,对于优先级较高的使命使用reserveThread()保留资源,对于优先级较低的使命可以选择延迟投递或动态增加线程池大小。

3、避免死锁

在多线程编程中,如果多个线程互相持有对方所需要的锁资源,就会发存亡锁。例如下面的代码:
  1. QMutex mutex1, mutex2;
  2. //线程1
  3. mutex1.lock();
  4. ...
  5. mutex2.lock(); //阻塞
  6. //线程2  
  7. mutex2.lock();
  8. ...
  9. mutex1.lock(); //阻塞
复制代码
避免死锁的一个常用战略是:对全部需要加锁的代码采用统一的加锁顺序,每个线程按相同顺序申请锁。

4、镌汰线程切换开销

线程切换是一个非常耗时的操纵,会带来较大的性能开销。我们应该只管镌汰线程切换的发生,例如:


  • 将密集计算的代码块集中在一个或几个线程中,避免在多个线程间切换
  • 避免线程中的循环中壅闭操纵(如休眠、加锁等),这会使该线程长时间占用CPU
  • 采用无锁编程,使用原子操纵和内存屏障指令实现线程安全操纵

通过本文的先容,希望你可以大概加深对Qt多线程编程的理解,在实际开发中合理使用多线程,提高应用程序的整体性能。下一篇文章将为你带来更多实战案例,进一步展示Qt多线程编程的实践本领。



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

惊落一身雪

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表