事件循环与线程 一

2023-11-09

 

初次读到这篇文章,译者感觉如沐春风,深刻体会到原文作者是花了很大功夫来写这篇文章的,文章深入浅出,相信仔细读完原文或下面译文的读者一定会有收获。

由于原文很长,原文作者的行文思路是从事件循环逐渐延伸到线程使用的讨论,译者因时间受限,暂发表有关事件循环的译文。另一半线程实用的译文将近期公布。文中有翻译不当的地方,还请见谅。

 

介绍

线程是qt channel里最流行的讨论话题之一。许多人加入了讨论并询问如何解决他们在运行跨线程编程时所遇到的问题。

快速检阅一下他们的代码,在发现的问题当中,十之八九遇到得最大问题是他们在某个地方使用了线程,而随后又坠入了并行编程的陷阱。Qt中创建、运行线程的“易用”性、缺乏相关编程尤其是异步网络编程知识或是养成的使用其它工具集的习惯、这些因素和Qt的信号槽架构混合在一起,便经常使得人们自己把自己射倒在了脚下。此外,Qt对线程的支持是把双刃剑:它即使得你在进行Qt多线程编程时感觉十分简单,但同时你又必须对Qt所新添加许多的特性尤为小心,特别是与QObject的交互。

本文的目的不是教你如何使用线程、如何适当地加锁,也不是教你如何进行并行开发或是如何写可扩展的程序;关于这些话题,有很多好书,比如这个链接给的推荐读物清单.  这篇文章主要是为了向读者介绍Qt 4的事件循环以及线程使用,其目的在于帮助读者们开发出拥有更好结构的、更加健壮的多线程代码,并回避Qt事件循环以及线程使用的常见错误。

先决条件

考虑到本文并不是一个线程编程的泛泛介绍,我们希望你有如下相关知识:

  • C++基础;
  • Qt 基础:QOjbects , 信号/槽,事件处理;
  • 了解什么是线程、线程与进程间的关系和操作系统;
  • 了解主流操作系统如何启动、停止、等待并结束一个线程;
  • 了解如何使用mutexes, semaphores 和以及wait conditions 来创建一个线程安全/可重入的函数、数据结构、类。

本文我们将沿用如下的名词解释,即

  • 可重入 一个类被称为是可重入的:只要在同一时刻至多只有一个线程访问同一个实例,那么我们说多个线程可以安全地使用各自线程内自己的实例。 一个函数被称为是可重入的:如果每一次函数的调用只访问其独有的数据(译者注:全局变量就不是独有的,而是共享的),那么我们说多个线程可以安全地调用这个函数。 也就是说,类和函数的使用者必须通过一些外部的加锁机制来实现访问对象实例或共享数据的序列化。
  • 线程安全  如果多个线程可以同时使用一个类的对象,那么这个类被称为是线程安全的;如果多个线程可以同时使用一个函数体里的共享数据,那么这个函数被称为线程安全的。

(译者注:   更多可重入(reentrant)和t线程安全(thread-safe)的解释:  对于类,如果它的所有成员函数都可以被不同线程同时调用而不相互影响——即使这些调用是针对同一个类对象,那么该类被定义为线程安全。 对于类,如果其不同实例可以在不同线程中被同时使用而不相互影响,那么该类被定义为可重入。在Qt的定义中,在类这个层次,thread-safe是比reentrant更严格的要求)

事件与事件循环

Qt作为一个事件驱动的工具集,其事件和事件派发起到了核心的作用。本文将不会全面的讨论这个话题,而是会聚焦于与线程相关的一些关键概念。想要了解更多的Qt事件系统专题参见 (这里[doc.qt.nokia.com]这里 [doc.qt.nokia.com] ) (译者注:也欢迎参阅译者写的博文:浅议Qt的事件处理机制一

一个Qt的事件是代表了某件另人感兴趣并已经发生的对象;事件与信号的主要区别在于,事件是针对于与我们应用中一个具体目标对象(而这个对象决定了我们如何处理这个事件),而信号发射则是“漫无目的”。从代码的角度来说,所有的事件实例是QEvent [doc.qt.nokia.com]的子类,并且所有的QObject的派生类可以重载虚函数QObject::event(),从而实现对目标对象实例事件的处理。

事件可以产生于应用程序的内部,也可以来源于外部;比如:

  • QKeyEvent和QMouseEvent对象代表了与键盘、鼠标相关的交互事件,它们来自于视窗管理程序。
  • 当计时器开始计时,QTimerEvent 对象被发送到QObject对象中,它们往往来自于操作系统。
  • 当一个子类对象被添加或删除时,QChildEvent对象会被发送到一个QObject对象重,而它们来自于你的应用程序内部

对于事件来讲,一个重要的事情在于它们并没有在事件产生时被立即派发,而是列入到一个事件队列Event queue)中,等待以后的某一个时刻发送。分配器(dispatcher )会遍历事件队列,并且将入栈的事件发送到它们的目标对象当中,因此它们被称为事件循环(Event loop). 从概念上讲,下段代码描述了一个事件循环的轮廓:

 

 

我们是通过运行QCoreApplication::exec()来进入Qt的主体事件循环的;这会引发阻塞,直至QCoreApplication::exit() 或者 QCoreApplication::quit() 被调用,进而结束循环。

这个“wait_for_more_events()” 函数产生阻塞,直至某个事件的产生。 如果我们仔细想想,会发现所有在那个时间点产生事件的实体必定是来自于外部的资源(因为当前所有内部事件派发已经结束,事件队列里也没有悬而未决的事件等待处理),因此事件循环被这样唤醒:

  • 视窗管理活动(键盘按键、鼠标点击,与视窗的交互等等);
  • socket活动 (有可见的用来读取的数据或者一个可写的非阻塞Socket, 一个新的Socket连接的产生);
  • timers (即计时器开始计时)
  • 其它线程Post的事件(见后文)。

Unix系统中,视窗管理活动(即X11)通过Socket(Unix 域或者TCP/IP)通知应用程序(事件的产生),因为客户端使用它们与X服务器进行通讯。 如果我们决定用一个内部的socketpair(2)来实现跨线程的事件派发,那么视窗管理活动需要唤醒的是

  • sockets;
  • timers;

这也是select(2) 系统调用所做的: 它为视窗管理活动监控了一组描述符,如果一段时间内没有任何活动,它会超时。Qt所要做的是把系统调用select的返回值转换为正确的QEvent子类对象,并将其列入事件队列的栈中,现在你知道事件循环里面装着什么东西了吧:)

为什么需要运行事件循环?

下面的清单并不全,但你会有一幅全景图,你应该能够猜到哪些类需要使用事件循环。

  • Widgets 绘图与交互: 当派发QPaintEvent事件时,QWidget::paintEvent() 将会被调用。QPaintEvent可以产生于内部的QWidget::update() ,也可以产生于外部的视窗管理(比如,一个显示被隐藏的窗口)。同样的,各种各样的交互(键盘、鼠标等)所对应的事件均需要事件循环来派发。
  • Timers: 长话短说,当select(2)或相类似的调用超时时,计时器开始计时,因此需要让Qt通过返回事件循环让那些调用为你工作。
  • Networking: 所以底层的Qt网络类(QTcpSocket, QUdpSocket, QTcpServer等)均被设计成异步的。当你调用read()时,它们仅仅是返回已经可见的数据而已; 当你调用write()时,它们仅是将写操作列入执行计划表待稍后执行。 真正的读写仅发生于事件循环返回的时候。 请注意虽然Qt网络类提供了相应的同步方法(waitFor* 一族),但它们是不被推荐使用的,原因在于他们阻塞了正在等待的事件循环。向QNetworkAccessManager这样的上层类,并不提供同步API 而且需要事件循环。

阻塞事件循环

在讨论为什么你永远都不要阻塞事件循环之前,让我们尝试着再进一步弄明白到底“阻塞”意味着什么。假定你有一个按钮widget,它被按下时会emit一个信号;还有一个我们下面定义的Worker对象连接了这个信号,而且这个对象的槽做了很多耗时的事情。当你点击完这个按钮后,从上之下的函数调用栈如下所示:

 

 

 

在main()中,我们通过调用QApplication::exec() (如上段代码第2行所示)开启了事件循环。视窗管理者发送了鼠标点击事件,该事件被Qt内核捕获,并转换成QMouseEvent ,随后通过QApplication::notify() (notify并没有在上述代码里显示)发送到我们的widget的event()方法中(第4行)。因为Button并没有重载event(),它的基类QWidget方法得以调用。 QWidget::event() 检测出传入的事件是一个鼠标点击,并调用其专有的事件处理器,即Button::mousePressEvent() (第5行)。我们重载了 mousePressEvent方法,并发射了Button::clicked()信号(第6行),该信号激活了我们worker对象中十分耗时的Worker::doWork()槽(第8行)。(译者注:如果你对这一段所描述得函数栈的更多细节,请参见浅议Qt的事件处理机制一

当worker对象在繁忙的工作时,事件循环在做什么呢? 你也许猜到了答案:什么也没做!它分发了鼠标点击事件,并且因等待event handler返回而被阻塞。我们阻塞了事件循环,也就是说,在我们的doWork()槽(第8行)干完活之前再不会有事件被派发了,也再不会有pending的事件被处理。

当事件派发被就此卡住时,widgets 也将不会再刷新自己(QPaintEvent对象将在事件队列里静候),也不能有进一步地与widgets交互的事件发生,计时器也不会在开始计时,网络通讯也将变得迟钝、停滞。更严重的是,许多视窗管理程序会检测到你的应用不再处理事件,从而告诉用户你的程序不再有响应(not responding). 这就是为什么快速的响应事件并尽可能快的返回事件循环如此重要的原因

强制事件循环

那么,对于需要长时间运行的任务,我们应该怎么做才会不阻塞事件循环? 一个可行的答案是将这个任务移动另一个线程中:在一节,我们会看到如果去做。一个可能的方案是,在我们的受阻塞的任务中,通过调用QCoreApplication::processEvents() 人工地强迫事件循环运行。QCoreApplication::processEvents() 将处理所有事件队列中的事件并返回给调用者。

另一个可选的强制地重入事件的方案是使用QEventLoop [doc.qt.nokia.com] 类,通过调用QEventLoop::exec() ,我们重入了事件循环,而且我们可以把信号连接到QEventLoop::quit() 槽上使得事件循环退出,如下代码所示:

 

QNetworkReply 没有提供一个阻塞式的API,而且它要求运行一个事件循环。我们进入到一个局部QEventLoop,并且当回应完成时,局部的事件循环退出。

当重入事件循环是从“其他路径”完成的则要非常小心:它可能会导致无尽的递归循环!让我们回到Button这个例子。如果我们再在doWork() 槽里面调用QCoreApplication::processEvents() ,这时用户又一次点击了button,那么doWork()槽将会再次被调用:

 

 

 

一个快速并且简单的临时解决办法是把QEventLoop::ExcludeUserInputEvents 传递给QCoreApplication::processEvents(), 也就是说,告诉事件循环不要派发任何用户输入事件(事件将简单的呆在队列中)。

同样地,使用一个对象的deleteLater() 来实现异步的删除事件(或者,可能引发某种“关闭(shutdown)”的任何事件)则要警惕事件循环的影响。 (译者注:deleteLater()将在事件循环中删除对象并返回)

 

 

可以看到,我们并没有用QCoreApplication::processEvents()  (从Qt 4.3之后,删除事件不再被派发 ),但是我们确实用到了其他的局部事件循环(像我们QEventLoop 启动的这个循环,或者下面将要介绍的QDialog::exec())。

切记当我们调用QDialog::exec()或者 QMenu::exec()时,Qt进入了一个局部事件循环。Qt 4.5 以后的版本,QDialog 提供了QDialog::open() 方法用来再不进入局部循环的前提下显示window-modal式的对话框

 

 

至此事件循环(event loop)的讨论告一段落,接下来,我们要讨论Qt的多线程:事件循环与线程二

 

请尊重原创作品和译文。转载请保持文章完整性,并以超链接形式注明原始作者主站点地址,方便其他朋友提问和指正。 

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

事件循环与线程 一 的相关文章

  • 如何为 qmlRegisterSingletonType 实现单例提供程序?

    我想在 QML 中使用 C 类作为 Singleton 实例 并认为我必须使用 qmlRegisterSingletonType 注册它们 此函数需要一个提供已注册 C 类的实例的函数 我在 Windows 上使用最新的 Qt 5 3 1
  • 使用绑定的 UWP 应用程序显示/隐藏按钮

    我有一个列表视图 其中显示项目名称和每个项目的一些按钮 这些按钮执行不同的操作 例如添加评论查看该项目的图像等 根据项目的不同 某些项目有时会禁用其中一些按钮 并且有些按钮在某些项目中将不可见 因此 我想在这段代码中使用数据绑定来实现两件事
  • 如何使 Python、QT 和 Webkit 在无头服务器上工作?

    我有 Debian Linux 服务器 我用它来做各种事情 我希望它能够完成一些我需要定期完成的网络抓取工作 这段代码可以是在这里找到 http bit ly QeqvzX import sys from PyQt4 QtGui impor
  • 打开和关闭附加窗口 (QML)

    目前我有一个通过以下方式打开的窗口 property variant win Button id testButton MouseArea onClicked var component Qt createComponent test qm
  • 仅在内部/外部抚摸路径?

    Given a QPainterPath http qt project org doc qt 4 8 qpainterpath html如何仅在路径的内侧或外侧边缘 或非闭合路径的左侧或右侧 描边路径 QPainter strokePat
  • Qt qDebug() 在 Windows shell 中不起作用

    我正在使用一个qDebug Qt 框架的printf屏幕上有东西 当我从 Qt Creator 运行应用程序时它工作得很好 但是当我尝试从 Windows 执行它时cmd它什么也没显示 为什么会发生这种情况 你必须添加 CONFIG con
  • QMutex 是否需要是静态的,以便此类实例的其他线程调用知道暂停其操作?

    从多个线程调用以下附加函数 我不希望数据重写附加 因为计数器尚未增加 除了当前使用 Append 的线程之外 这是否会挂起所有进入的线程 或者其他线程会继续运行而不追加数据吗 互斥锁是否需要是 静态 的 或者每个实例都知道要暂停操作吗 如果
  • 在没有加载器的情况下实例化内联组件

    有没有办法实例化内联Component 即在同一文件中定义 而不使用Loader 我不太关心使用的性能影响Loader因为我要用很多东西污染我的文件Loader包装纸 我发现从 JavaScript 创建动态 QML 对象 http doc
  • 第一次信号发射后自动断开

    我正在从文件加载网页 然后替换其中的一些 html self template web page QtWebKit QWebPage self template web page mainFrame load QtCore QUrl tem
  • SGC GUI 和 Pygame Widget 实现

    您好 我正在尝试使用 Pygame 编写一个简单的应用程序 我进行了各种搜索 发现获取用户输入的最佳方法是使用第 3 方 GUI 我为此目的找到了简单的游戏代码 下面 您可以找到我的基本代码 它在脚本的同一路径中查找图像并在屏幕上按顺序替换
  • 如何在占位符中动态生成的标签之间创建换行符?

    这是文件后面代码中的以下代码Page Load event LinkButton linkButton new LinkButton linkButton ID LinkButtonDynamicInPlaceHolder1Id i lin
  • 清除pyqt中布局中的所有小部件

    有没有办法清除 删除 布局中的所有小部件 self plot layout QtGui QGridLayout self plot layout setGeometry QtCore QRect 200 200 200 200 self r
  • 如何声明一个带有成员函数指针的函数

    我有一个类 其中的成员变量指向库对象 class myClassA private libraryClass libraryObject 该库类发出事件 以字符串为特征 并提供一种机制 允许客户端类指定在发出事件时应调用的成员函数 因此在m
  • QTableView 并双击一个单元格

    我正在开发测试用例编辑器 该编辑器包含 USART 传输和接收数据包格式 编辑器是一个表格视图 发送和接收数据包的长度为八个字节 例如 0x01 0x02 0x03 0x08 它在我的第五和第六栏中 现在 我希望此列中的单元格为只读 但是当
  • PyQt4 QPalette 不工作

    btn QtGui QPushButton Button self palettes btn palette palettes setColor btn backgroundRole QtCore Qt green btn setPalet
  • Qt中Q_PROPERTY的意义是什么?

    我无法理解 Q PROPERTY 的用法 Q PROPERTY 如何帮助程序具有防御性 它是干什么用的 我看过这个论坛 但确实无法应用 我已经理解了这个例子 但不明白它的用法 这是一个例子 我能从中得到什么 我知道阅读将赋予只读特权 wri
  • Qt 5.1.1 与 Visual Studio 2012 - 这些 QT 版本无法访问

    打开 Visual Studio 时出现此错误 我安装自http qt project org downloads http qt project org downloads 适用于 Windows 64 位的 Qt 5 1 1 VS 20
  • PyQt4 信号和槽

    我正在使用 PyQt4 编写我的第一个 Python 应用程序 我有一个 MainWindow 和一个 Dialog 类 它是 MainWindow 类的一部分 self loginDialog LoginDialog 我使用插槽和信号 这
  • Qt(在 Windows 上)将权限级别设置为“requireAdministrator”

    我正在使用 Qt Creator 并努力制作 exe文件默认以管理员身份运行 在线阅读所有解决方案我试图将这一行放入我的 pro file QMAKE LFLAGS MANIFESTUAC level requireAdministrato
  • 通过引用传递 [C++]、[Qt]

    我写了这样的东西 class Storage public Storage QString key const int value const void add item QString int private QMap

随机推荐

  • 2016去哪儿编程题:5-血型遗传检测

    题目描述 血型遗传对照表如下 父母血型 子女会出现的血型 子女不会出现的血型 O与O O A B AB A与O A O B AB A与A A O B AB A与B A B AB O A与AB A B AB O B与O B O A AB B与
  • shell 数组(字符串下标)

    现在游戏开的服务器越来越多了 每次用ssh操作都要写ip地址 很烦 也容易出错 所以要自己搞个服务器名到ip的映射 map anahost count 0 temp cat home linwencai sh HOST while read
  • ubuntu18.04合并pdf文件

    以前使用pdftk比较常见 但是pdftk的更新似乎没有跟上 改用pdfunite轻松解决 pdftk原来使用apt安装 现在改成用snap安装pdftk sudo snap install pdftk pdftk合并命令为 pdftk p
  • 洛谷 P1200 [USACO1.1]你的飞碟在这儿Your Ride Is Here

    题目链接 https www luogu com cn problem P1200 include
  • C++函数基础

    一 函数的定义和使用 1 函数的定义 类型说明符 函数名 含类型说明的形参表 语句序列 如 int GetSum int a int b return a b 2 形式参数 形式参数的作用是实现主函数与被调函数之间的联系 3 函数的返回值和
  • 【ubuntu】ubuntu实体机与windows互传文件(两台电脑)

    先记录一些命令 dpkg list 查看软件列表 sudo apt get purge remove 包名 purge是可选项 写上这个属性是将软件及其配置文件一并删除 如不需要删除配置文件 可执行sudo apt get remove 包
  • Python列表操作中extend和append的区别

    1 用法 append 用于在列表末尾添加新的对象 输入参数为对象 extend 用于在列表末尾追加另一个序列中的多个值 输入对象为元素队列 2 相同点 两个都是对列表即list进行的操作 具体句法可以写为 list1 append obj
  • 解决EXPLORER应用程序错误,桌面出不来

    打开运行 输入CMD 输入for 1 in windir system32 dll do regsvr32 exe s 1 意思是注册所有DLL组件 一般都能解决问题 转载于 https blog 51cto com feifei888 4
  • word文件doc、docx转pdf

    综合类管理系统不管是自研还是外包项目都会被客户或者产品经理要求 实现word导出 excel导出 pdf导出等功能 其实pdf导出呢 有很多种方式 我实现过的就有两种 接下来呢 就说说其中的一种 就是当你已经实现了word导出 或有明确的要
  • 粉丝文化:抖音广告短视频美妆营销中,男明星比女明星更带货?

    1996年 木村拓哉为佳丽宝拍摄了一支口红广告 这条广告轰动一时 代言的口红两个月就卖出了300万支 从此 男明星就成了美妆品牌的宠儿 众多美妆品牌开始启用男明星代言人 男明星为何有如此强力的带货潜力 美妆品牌如何在短视频时代占得先机 抖音
  • 阿里天池比赛——街景字符编码识别

    文章目录 前言 一 街景字符编码识别 1 目标 2 数据集 3 指标 总结 前言 之前参加阿里天池比赛 好久了 一直没有时间整理 现在临近毕业 趁论文外审期间 赶紧把东西整理了 5月底学校就要让我们滚蛋了 哭哭哭 大运会的牺牲品 一 街景字
  • 赛马游戏的java设计_赛马游戏源码

    0 Intro pos g setClip 10 10 HorseMidlet imgIntro 0 getWidth HorseMidlet i mgIntro 0 getHeight g drawImage HorseMidlet im
  • 面试题记1

    希望各位看客们能积极提供答案 1 125874和它的两倍251748 包含着同样的数字 只是顺序不同 找出最小的正整数x 使得2x 3x 4x 5x 和6x都包含有相同的数字 2 求100 各位数之和 3 是用从1到9所有数字 将其任意的连
  • Notion?Roam?OneNote? 不要再用这些垃圾做笔记啦

    双向链接 最近因为Roam Research 双向链接在笔记圈子里火了起来 Notion也在准备做了 那么双向链接是什么呢 我用我的我关于管道的一则笔记给大家讲明白 管道的实现 Linux里 管道实现的原理是 Shell进程先调用pipe创
  • 浅谈 qmake 之 shadow build

    shadow build shadow build 是什么东西 就是将源码路径和构建路径分开 也就是生成的makefile文件和其他产物都不放到源码路径 以此来保证源码路径的清洁 这不是qmake独创的东西 cmake中早就使用这个东西了
  • 性能测试_Day_10(负载测试-获得最大可接受用户并发数)

    目录 如何理解负载测试 如何实现负载测试 jpgc Standard Set插件安装 jpgc Standard Set使用方法 负载测试分析指标 获得最大可接受用户并发数 区间值 负载测试分析指标 获得最大可接受用户并发数 真实值 负载测
  • 阅读论文《Deep Bilateral Learning for Real-Time Image Enhancement》

    这是2017 siggraph的一篇论文 寒假boss让我看这篇论文我没怎么看懂 最近在公司实习 发现该论文的成果已经移到手机端上了 效果还非常不错 这里我重新温习了一下这篇论文 发现有许多可以借鉴的地方 是一篇非常不错的论文 这里重新叙述
  • 我碰到avs错误

    1 写好的avs脚本用播发器不能播放 并且报unexpected chatacter 错误 解决办法 1 尽管avs支持汉语文件路径 但是仍要确认标点符号是否为英文状态下 2 将AVS脚本用记事本打开 重新存为并把编码格式修改成ASNI格式
  • 数值计算方法python实现

    包括 泰勒级数展开 差分逼近微分 二分法求解 试位法求解 迭代法求根 牛顿法求根 正割法 贝尔斯托法多项式求跟 多项式回归 牛顿差商插值 拉格朗日插值法 三次样条插值法 二次样条插值法 高斯消元法 求解线性代数方程组 代码在我的github
  • 事件循环与线程 一

    初次读到这篇文章 译者感觉如沐春风 深刻体会到原文作者是花了很大功夫来写这篇文章的 文章深入浅出 相信仔细读完原文或下面译文的读者一定会有收获 由于原文很长 原文作者的行文思路是从事件循环逐渐延伸到线程使用的讨论 译者因时间受限 暂发表有关