邮箱中的Qt线程设计

news2024/11/16 1:41:37

邮箱(deepin-mail)主要使用Qt框架开发,是一个有大量并行任务且需要监控进度和结果的项目,任务的优先级调整和支持取消回滚也是必不可少。Qt已经为我们提供了多种线程设计的方法,可以满足绝大部分使用场景,但是每一种方案单拎出来都不能很恰到好处的在项目中使用,本文主要对项目中的线程设计进行介绍,通过将多个Qt线程方案相结合,衍生出一种适合邮箱这类型项目的应用方法。

浅谈Qt中的线程方法

QThread

基于QThread方案,需要子类化QThread实现run()方法,并在合适的时候调用start(),在这之前需要做好数据交换,并在线程执行过程中通过跨线程的connect()方法将数据传出,这里需要注意不能将连接参数设置为Qt::DirectConnection,因为线程的数据需要通过事件循环传递来保证安全,在跨线程的connect()方法中参数即使使用了引用Qt也依然会在emit时帮你额外触发一次参数拷贝,影性能。

如果不是开销非常大的任务像这样直接继承其实不受推荐,现在更倾向于使用组合的方式,类化QObject来创建一个Worker类,添加一个QThread成员变量,将Worker对象移动到启动后的QThread对象线程中,这样Worker信号触发的槽函数都会在QThread对象启动的线中运行。由于QThread也是QObject派生类, 偷懒的人可以让Worker接继承QThread然后moveToThread(this),当然这样做需要对QThread的理解更 透彻一些,所以官方也不建议这种写法因为QThread是被设计成一个管理线程的类不应参与过多业务逻辑。

基于QThread的方案尤其要注意定时器和套接字的析构问题,确保他们在创建的线程中使用和析构,QThread的使用者会不注的将他们在线程中创建和使用,却在QThread自己的析构函数中析构(QThread对象的析构是在他所在的线程而不是自己开启的线程)。

QRunnable

基于QRunnable的方案,将务分解成QRunnable对象直接放入QThreadPool中执行,使用上和QThread一样我们完成他的run()方法,但是能且只能通过QThreadPool来执行,自身不包含其他的功能特性,开销很小。但就是因为这样,他和外界交换数据或者线程同步就变得相当麻烦,由于QRunnable不是QObject派生类,无法使用Qt的信号槽,不做一些处理很难优雅地将进度和结果抛出,如果用同时继承QRunnableQObject的方式进行来添加信号槽机制不如直接使用QThread了。还有一种方式,在QRunnable对象中保存一些传入的引用或指针来做消息传递,这样数据可以通过原子 变量或互斥锁来实现更新,指针可以使用元对象系统中的方法QMetaObject::invokeMethod()通过事件循环传出消息,但实际运用起来依旧麻烦,每个数据包括指针你都要考虑他的跨线程竞争问题,不得不控制参数的数量,将每个任务尽可能的切割成更小的任务。

QtConcurrent

一种基于QtConcurrent框架的方案,是Qt的高级别线程特性,支持多种线程管理的方法, 只要把方法或者lambda表达式传入run()并指定一个线程池(默认是全局线程池QThreadPool::globalInstance())就完成了开线程执行直到返回结果的一系列流程,它会自动将任务分配到可用的处理器核心上执行,最大化利用现在核心越来越多的cpu架构。它的run()方法重载数量非常多,包括 异步和同步等待线程执行完成的方法。选择其中的异步方法,就可以通过监控返回的QFuture对象来得到线程状态和结果,主要方法的官方描述如下:

Concurrent Map and Map-Reduce
QtConcurrent::map() applies a function to every item in a container, modifying the items in-place.
QtConcurrent::mapped() is like map(), except that it returns a new container with the modifications.
QtConcurrent::mappedReduced() is like mapped(), except that the modified results are reduced or folded into a single result.
Concurrent Filter and Filter-Reduce
QtConcurrent::filter() removes all items from a container based on the result of a filter function.
QtConcurrent::filtered() is like filter(), except that it returns a new container with the filtered results.
QtConcurrent::filteredReduced() is like filtered(), except that the filtered results are reduced or folded into a single result.
Concurrent Run
QtConcurrent::run() runs a function in another thread.

横向比较以上方案

以上方案并没有一种是完全优于另一种的,每种都有它适应的场景,灵活运用满足项目的需要是最重要的。

邮箱中的线程方案

心路历程
Qt Concurrent方案中 基于future的机制非常适合邮箱项目做前后端分离,后端只要提供 QFuture 对 象出去被前端监控,其他的工作包括任务调度都在后端内部完成,实现解耦。但总体上它的设计初 衷更多是为了使用简单而不是功能强大,对并行处理的线程增加了映射、 过滤 和规约算法却削弱了 对线程内部的控制和线程之间的消息传递、同步能力,而 QFuture 对象提供了 pause ()和 cancel ()等方法只能影响线程池中还未执行的任务。
希望使用QtConcurrent特性的同时还要解决一些实际问题,要能够在线程池中对不同优先级的线程进行排序,要能掌握耗时任务的实时进度并能够对其暂停和取消,操作的影响具体到任务中操作数据库、读写文件或者和服务器交互中的某一步,这样在部分取消后可以做一些回滚来保证任务的原子性。
既然Qt使用了future来命名,其实Qt已经实现了 future-promise机制 ,还在自己的源码中大量的使用 。如果观察 QFuture QThreadPool 的源码,时不时就会看到一个叫 QFutureInterface 的类,Qt的帮助文档中不包含相关资料,但是别看他叫做"interface",其实他是可以直接使用的,并且拥有着满足项目需要的方法。有兴趣的同学可以阅读相关源码来了解,如果在源码中看到以下的描述不要紧张,一直追溯到Qt5的最后一个版本,这些接口也是存在并且稳定的。

方案改造
为了更好的利用这个特性,需要对它进行了改造以接地气一些,我们通过使用 QThreadPool +
QRunnable 方案中的设计思路来控制线程池任务 。
首先继承 QRunnable 创建一个类模板用于后面衍生出各式各样的任务:
template < typename T >
class AbstractTask : public QRunnable
{
public :
QFuture < T > future ();
protected :
inline void reportStarted ();
inline void setProgressRange ( int minimum, int maximum);
inline void setProgressValueAndText ( int progressValue, const QString &progressText =
"" );
inline void reportResult ( const T &result, const int &index = - 1 );
inline void reportFinished ( const T *result = 0 );
virtual void run () = 0 ;
private :
QFutureInterface < T > d ;
};
模板参数是每个任务想要对外提供的返回结果,可以只返回错误码和错误描述用于表示执行结果,也可以添加更多的参数比如同时将下载的文件通过future返回。不用担心额外的拷贝开销,因为另外一个让人省心的地方是, QFutureInterface 已经通过引用计数为自己实现了隐式共享。
template < typename T >
class QFutureInterface : public QFutureInterfaceBase
{
public :
QFutureInterface ( State initialState = NoState )
: QFutureInterfaceBase ( initialState )
{
refT ();
}
QFutureInterface ( const QFutureInterface & other )
: QFutureInterfaceBase ( other )
{
refT ();
}
~ QFutureInterface ()
{
if (! derefT ())
resultStoreBase (). template clear< T >();
}
...
}
QFutureInterface 通过原子变量来实现引用计数,提供一个平台无关的原子操作,但并不是所有的处理器都支持 QAtomicInt ,如今国产芯片百家争鸣,如果你是特殊的架构,使用前检测一下当前处理器是否支持某个API是很重要的。使用原子变量会比使用互斥锁的方式更加简单和高效:
class RefCount
{
public :
inline RefCount ( int r = 0 , int rt = 0 ): m_refCount ( r ), m_refCountT ( rt ) {}
inline bool ref () { return m_refCount.ref(); }
inline bool deref () { return m_refCount.deref(); }
inline int load () const { return m_refCount.load(); } inline bool refT () { return m_refCountT.ref(); }
inline bool derefT () { return m_refCountT.deref(); }
inline int loadT () const { return m_refCountT.load(); }
private :
QAtomicInt m_refCount ;
QAtomicInt m_refCountT ;
};
QFutureInterface 通过 future ()方法创建出的 QFuture 都会存有一份自己的引用实例,参与了引用计数 的计算。只有当所有的 QFutureInterface 对象都被析构(包括 QFuture 中的),他们所指向的 result ()结果空间才也会释放。出于灵活同一个任务是可以返回多个 QFuture 对象分发到不同的模块以被监控的, 但在任务完成后记得重置 QFuture 以释放内存,赋值为一个 QFuture ()即可。
template < typename T >
class QFuture
{
public :
explicit QFuture ( QFutureInterface < T > * p )
: d (* p )
{ }
mutable QFutureInterface < T > d ;
}
template < typename T >
class QFutureInterface : public QFutureInterfaceBase
{
inline QFuture < T > future ()
{
return QFuture < T >( this );
}
}
使用案例

上图是任务流转的一个简要流程,结合这个流程下面将给出项目中的一个实现,一个为自己的账号创建邮件目录的任务:

class CreateMailFolderTask : public AbstractTask < ResultResponse >
{
public :
CreateMailFolderTask ( QSharedPointer < ProtocolEngine > engine , QSharedPointer < ProtocolCache > cache );
void run ();
void initParam ( const QString & folderName );
private :
QSharedPointer < ProtocolEngine > m_engine ;
QSharedPointer < ProtocolCache > m_cache ;
QString m_folderName ;
};
...
void CreateMailFolderTask :: run ()
{
setProgressRange ( 0 , 100 );
reportProgressValueAndText ( 0," Started " );
//do something
const ResultResponse & result = m_engine ->createMailFolder ( m_folderName );
reportProgressValueAndText ( 50," Ongoing");
//different processing according to d . isPaused () , d . isCanceled ()
if ( RESULT_SUCCESS == result . type )
m_cache ->createFolderId ( m_folderName ); //do something
reportProgressValueAndText ( 100," Finished");
reportResult ( result );
reportFinished ();
}
首先按我们的模板实现一个创建目录的任务,在任务的run()方法中实现相关功能,这时候就可以根据需要自由的通过多种 report ()方法将进度、状态描述和结果抛出,以便外部可以在每个节点获取当前任务的状态,根据是否被暂停或者被取消等通过 QFuture 设置的状态来做出不同的处理,如果有必要比如在邮箱项目中,我们传递了一个 QAtomic 原子变量到任务甚至子任务中,进行更加精的控制。类中有两个成员变量 m_engine m_cache ,这个是项目中用于执行邮件协议和本地缓存代码的控制类,线程安全,不做过多扩展说明。接下来是使用:
QFuture < ResultResponse > createFolder ( const QString & folderId ){
CreateMailFolderTask * task = new CreateMailFolderTask ( m_protocolEngine , m_protocolCache );
task -> initParam ( folderId );
QFuture < PrepareResponse > future = task -> future ();
emit sigNewTask ( task );
return future ;
}
我们创建了一个任务,但是任务并不需要立刻开始,而是通过信号将task抛出等待处理,可以在合适的时候通过线程池 pool -> tryStart ( task )来执行,可以丢在数据结构中保存下来进行优先级排序后等待唤起,还可以格式化存储到文件中保存退出等待下次应用启动后继续执行。拿到 QFuture 对象的模块立刻就能够进行监控,是否开始、是否结束和进度都可以通过 QFuture 的方法获取或使用 QFutursSynchronizer 组合监控,也可以通过 QFutureWatcher 监控 QFuture 实现被动处理,这个具体看看官方说明即可:
QFuture represents the result of an asynchronous computation.
QFutureIterator allows iterating through results available via QFuture .
QFutureWatcher allows monitoring a QFuture using signals-and-slots.
QFutureSynchronizer is a convenience class that automatically synchronizes several
QFutures.
QFutureWatcher < ResultResponse > watcher ;
watcher . setFuture ( future );
QObject :: connect (& watcher , & QFutureWatcher < ResultResponse >:: progressValueChanged ,
[ = ]( int progressValue ) {
progressValue ; //do something });
任务的返回可以通过 QFuture result ()方法获取,如果是逐步抛出结果的批处理任务,可以通过 results ()或者 resultAt (int index)方法获取已取得的结果列表。 result ()的提前调用不会产生错误,它会阻塞当前的线程,等待任务完成后得到结果才会继续向下执行,也可以主动调用 waitForFinished () 方法阻塞等待任务完成达到一样的效果。阻塞等待可以不用为了耗时极短的任务去写监控代码,也为写单元测试代码带来了非常大的便利性:
# include < gtest / gtest . h >
TEST_F ( ut_session , createFolder ){
QFuture < ResultResponse > future = session -> createFolder ("MyBox");
future . waitForFinished ();
EXPECT_TRUE ( ResultCode :: Success == future . result (). code );
}
小结
总结一下,Qt future-promise结合 QRunnable 的方案十分灵活,其实还有很多特性没有在此演
示,我们已经将它落地在邮箱项目中,接口稳定运行,取得了不错的效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/136946.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

钧瓷产业基础架构(SCA架构)是钧瓷产业发展的核心引擎

《钧瓷内参》独立客观&#xff0c;原创前瞻&#xff0c;深度评论&#xff0c;定期推送 钧 瓷 内 参 第3期&#xff08;总第335期&#xff09; 2023年1月3日 钧瓷产业数字化发展的下一个阶段——钧瓷共同体&#xff0c;是钧瓷产业数字化发展的必然趋势。 实现钧瓷共同体的路线…

JavaSE从基础到入门:String类的学习

前言 字符串广泛应用 在 Java 编程中&#xff0c;在 Java 中字符串属于对象&#xff0c;Java 提供了 String 类来创建和操作字符串。 1.String类的方法 1.字符串的构造方法 使用常量串构造 String s1 "hello world"; System.out.println(s1);直接newString对象…

【Linux】Linux基本权限

作者&#xff1a;一个喜欢猫咪的的程序员 专栏&#xff1a;《Linux》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 目录 1.shell命令以及运行原理 2.Linux权限 2.1Linux权限的概念 2.2Linux上用户…

DALLE2-文本图像生成

文章目录摘要算法解码器prior图像处理变体插值文本差异限制论文&#xff1a; 《Hierarchical Text-Conditional Image Generation with CLIP Latents》github&#xff1a; https://github.com/lucidrains/DALLE2-pytorchhttps://github.com/LAION-AI/dalle2-laion摘要 CLIP已经…

window环境安装mysql8.0.12版本的安装、配置(详细步骤图解)

目录一、mysql官网下载网址二、下载步骤三、安装步骤四、测试链接一、mysql官网下载网址 mysql官网下载网址 https://www.mysql.com/ 二、下载步骤 浏览器输入https://www.mysql.com/网址&#xff0c;点击【DownLoads】&#xff0c;如下图&#xff1a; 向下滑动网页&#x…

软件测试面试注意事项汇总

面对最近的复工热潮&#xff0c;不少求职者也开始蠢蠢欲动准备找工作了。相信大家都知道疫情下面试求职的压力是有史以来最大的&#xff0c;我们唯一能做好的只有积极的准备面试&#xff0c;让自己可以更加从容的面对的面试官的提问。下面小编为大家汇总了软件测试面试过程中的…

CSDN官方猿如意工具体验

2022年注定是不平凡的一年&#xff0c;2022再见&#xff0c;2023你好&#xff01; 2023愿我们发财&#xff0c;被爱&#xff0c;一路好运常在&#xff01;愿所念之人平安喜乐&#xff0c;所想之事顺心如意&#xff0c;岁岁常欢喜&#xff0c;万事皆胜意&#xff01; 猿如意工具…

中间件:Win10安装运行Kafka

一、JDK环境安装配置 可参考&#xff1a;百度安全验证 二、Zookeeper安装配置 1、下载 &#xff1a; Index of /dist/zookeeper/zookeeper-3.4.9 2、解压到本地&#xff0c;目录不要带中文符号&#xff0c;保证纯英文 3、zoo.cfg修改 4、cmd运行&#xff0c;使用命令zkServe…

ConcurrentHashMap 线程安全

JDK1.7 结构 数据结构是数组segment对象&#xff0c;采用segment分段锁和CAS保证并发。 加锁 JDK1.7中的ConcurrentHashMap是由 segment数组结构和 HashEntry 数组结构组成&#xff0c;即 ConcurrentHashMap把哈希桶切分成小数组(Segment )&#xff0c;每个Segment 有n个 Hash…

终于!Linaro 加盟 Zephyr 项目

导读为物联网构建实时操作系统的开源协作项目 Zephyr 项目宣布&#xff0c;Linaro 有限责任公司以白金会员的身份加盟该项目。Linaro是一家为 ARM 架构开发开源软件的协作工程组织&#xff0c;也是全球性机构&#xff0c;其 35 个成员中不乏来自多个行业部门的龙头企业。Linaro…

嵌入式实时操作系统的设计与开发(一)

以一款简单、易学的嵌入式开发平台ARM Mini2440&#xff08;CPU是三星ARM 9系列的ARM S3C2440&#xff09;为例&#xff0c;通过具体代码实现&#xff0c;介绍如何从裸板入手设计简单的轮询系统、前后台系统&#xff0c;以及如何一步一步在ARM Mini2440上编写RTOS内核&#xff…

Spring之配置非自定义Bean

目录 一&#xff1a;概述 二&#xff1a;代码演示 1)配置Druid数据源交由Spring管理 一&#xff1a;概述 以上在xml中配置的Bean都是自己定义的&#xff0c; 例如&#xff1a;UserDaolmpl&#xff0c; UserServicelmpl。但是&#xff0c; 在实际开发中有些 功能类并不是我们…

包装器和绑定器std::bind和std::function的回调技术

回调函数 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针&#xff08;地址&#xff09;作为参数传递给另一个函数&#xff0c;当这个指针被用来调用其所指向的函数时&#xff0c;我们就说这是回调函数。回调函数不是由该函数的实现方直接调用&#xff0c;而是在…

低代码搭建门店管理之收发货管理系统

随着电商的深入&#xff0c;不少门店都开始采用线上模式进行销售了&#xff0c;并且有的门店线上销售更是比线下销售更加火爆。因此&#xff0c;线上销售务必会涉及到收发货这个步骤。以前线上销售刚兴起的时候收发货只靠人工纸质化登记就能搞定&#xff0c;但是随着线上销售的…

盘点这些年稚晖君的DIY项目,看看他的技术栈有多强

近日&#xff0c;知名极客稚晖君在个人微博发文称自己将离职创业&#xff0c;开启一段新的旅程&#xff0c;“天才少年”将在机器人领域继续发光发热。 自2020年初发布第一个出圈视频《技术宅UP耗时三个月&#xff0c;自制B站最强小电视&#xff01;》以来&#xff0c;稚晖君共…

Vue实例的基本属性,computed计算属性,watch监听属性以及过滤器filters

目录 一、Vue实例的属性 二、Vue实例的计算属性&#xff1a;computed。计算属性结果会被缓存起来&#xff0c;当依赖的响应式属性发生变化时&#xff0c;才会重新计算&#xff0c;返回最终结果。 三、Vue实例的状态监听属性&#xff1a;watch&#xff0c;可以对元素的值的变…

JVM垃圾回收相关算法-垃圾标记阶段

文章目录学习资料垃圾回收概念概述垃圾回收相关算法垃圾标记阶段&#xff1a;对象存活判断引用计数算法可达性分析算法&#xff08;或根搜索算法、追踪性垃圾收集&#xff09;【Java使用算法】基本思路GC Roots对象的finalization机制对象处于三种可能的状态具体过程学习资料 …

WebDAV之葫芦儿·派盘+WebDAV Nav Lite

WebDAV Nav Lite 支持WebDAV方式连接葫芦儿派盘。 支持连接所有WebDAV服务器、云存储、NAS设备的管理工具,并可以直接管理设备内的文件?那快来试下WebDAV Nav Lite自动同步与管理工具吧。 WebDAV Nav Lite允许您

【Bootstrap】CSS全局样式

目录 一、HTML5文档类型 二、移动设备优先 三、禁用移动设备上的缩放功能 四、布局容器 1. container 类 ​2. container-fluid 类 五、标题 六、页面主体 七、文本 1. 内联文本元素 2. 文本对齐 ​3. 改变大小写 八、列表 1. 无序列表 2. 有序列表 3. 无样式列…

Hadoop数据仓库有哪些特征?

数据仓库(英语&#xff1a;Data Warehouse&#xff0c;简称数仓、DW),是一个用于存储、分析、报告的数据系统。数据仓库的目的是构建面向分析的集成化数据环境&#xff0c;分析结果为企业提供决策支持(Decision Support)。 数据仓库本身并不“生产”任何数据&#xff0c;其数据…