Windows 的消息机制

本文深入探讨Windows消息机制,包括消息队列、消息生命期、消息处理流程及消息死锁问题。详细介绍了系统消息队列与线程消息队列的工作原理,消息的产生、处理与优先级,以及如何避免消息死锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.       消息队列

说到消息机制,可能连最初级的 Windows 程序员都会对消息队列(MessageQueue)这个名词耳熟(不过不见得能详)。对于这样一个基本概念,Windows 操作系统提供的针对消息队列的API 却少的可怜(GetQueueStatus、GetInputState、GetMessageExtraInfo、SetMessageExtraInfo),而且,这些 API 的出镜率也相当的低,甚至有不少经验丰富的程序员也从来没有使用过它们。在 Windows Mobile 上,这些 API 干脆付诸阙如,不过有一个同样极少使用的GetMessageQueueReadyTimeStamp 函数在充门面。

这一切,都归功于在 API 层极好的封装性,减少了开始接触这个平台时需要了解的概念。但是,对于我们这样既想知其然,又想知其所以然的群体,还是有必要对消息队列有充分的了解。

1.1.      系统消息队列

这是一个系统唯一的队列,输入设备(键盘、鼠标或者其他)的驱动程序会把用户的操作输入转化成消息放置于系统队列中,然后系统会把此消息转到目标窗口所在线程的消息队列中等待处理。

1.2.      线程消息队列(应用程序消息队列)

应用程序消息队列这个名称是历史遗留,在 32 位(以及之后的 64 位)系统中,正确的名称应该是线程消息队列。每一个GUI线程都会维护这样一个线程消息队列。(这个队列只有在线程调用 User 或者 GDI 函数时才会创建,默认并不创建)。然后线程消息队列中的消息会被本线程的消息循环(有时也被称为消息泵)派送到相应的窗口过程(也叫窗口回调函数)处理。

2.       消息的生命期

2.1.      消息的产生

消息产生的源头有两个,一个是系统,一个是应用程序。系统产生的消息又可以大致分为两类,一类是由输入设备导致的,例如 WM_MOUSEMOVE,一类是User部分(或者是系统内的其他部分通过User部分)为了实现自身的正常行为或者管理功能而主动生成的,如 WM_WINDOWPOSCHANGED。

产生的方式也有两种,一种称为发送(Send),另一种称为投递(Post,也有译作张贴的),对应于大家极为熟悉的两个 API,SendMessage 和 PostMessage。系统产生的消息,虽然我们看不到代码,不过我们还是可以粗略地划拨一下,基本上所有的输入类消息,都是以投递的方式抵达应用的,而其他的消息,则大部分是采取了发送方式。

至于应用程序,可以随意选用适合自己的消息产生方式。

2.2.      消息的处理

在绝大部分情况下,消息总是有一个目标窗口的,因此,消息也绝大部分是被某个窗口所处理的。处理消息的地方,就是这个窗口的回调函数。

窗口的回调函数,之所以被称作“回调”,就是因为这个函数一般并不是由用户(程序员)主动调用它的,而是系统认为在恰当的时候对它进行调用。那么,这个“恰当的时候”是什么时候呢?根据消息产生的方式,“恰当的时候”也有两个时机。

第一个时机是,DispatchMessage 函数被调用时,另一个时机是SendMessage 函数被调用时。

我们正常情况下以系统处理对一个顶级窗口的关闭按钮的鼠标左键点击事件为例来说明。

这个点击事件完成的标志性消息是 WM_NCLBUTTONUP,表示在一个窗口的非客户区的鼠标左键释放动作,另外,这个鼠标消息的其他数据中会表明,发生这个动作的位置是在关闭按钮上(HTCLOSE)。这是一个鼠标输入事件,从前文可以知道,它会被系统投递到消息队列中。

于是,在消息循环中GetMessage 的某次执行结束后,这个消息被取到了 MSG 结构里。从文章开头的消息循环代码可知,这个消息接下来会被 TranslateMessage 函数做必要的(事实上是“可能的”)翻译,然后交给 DispatchMessage 来全权处理。

DispatchMessage 拿到了 MSG 结构,开始自己的一套办事流程。

首先,检查消息指定的目标窗口句柄。看系统内(实际上是本线程内)是不是确实存在这样一个窗口,如果没有,那说明这个消息已经不会有需要对它负责的人选了,那么这个消息就会被丢弃。

如果有,它就会直接调用目标窗口的回调函数。终于看到,我们写的回调函数出场了,这就是“恰当的时机”之一。当然,为了叙述清晰,此处省略了系统做的一些其他处理。

这样,对于系统来说,一条投递消息就处理完成,转而继续 GetMessage。

不过对于我们上面的例子,事情还没有完。

我们都清楚,对于 WM_NCLBUTTONUP 这样一条消息,通常我们是无暇去做额外处理的(正事还忙不过来呢……)。所以,我们一般都会把它扔到那个著名的垃圾堆里,没错,DefWindowProc。尽管如此,我们还是可以看出,DefWindowProc其实已经成了我们的回调函数的一个组成部分,唯一的差别在,这个函数不是我们自己写的而已。

DefWindowProc 对这个消息的处理也是相当轻松,它基本上没有做什么实质性的事情,而是生成了另外一个消息,WM_SYSCOMMAND,同时在 wParam 里指定为 SC_CLOSE。这一次,消息没有被投递到消息队列里,而是直接 Send 出来的。

于是,SendMessage 的艰难历程开始。

第一步,SendMessage 的方向和DispatchMessage 几乎一模一样,检查句柄。

第二步,事情就来了,它需要检查目标窗口和自己在不在一个线程内。如果在,那就比较好办,按照 DispatchMessage 趟出来的老路走:调用目标窗口的回调函数。这,就是“恰当的时机”之二。

可是要是不在一个线程内,那就麻烦了。道理很简单,别的线程有自己的运行轨迹,没有办法去让它立即就来处理这个消息。

现在,SendMessage该怎么处理手里的这个烫手山芋呢?(作者注:写到此处时,很有写上“欲知后事如何,且听下回分解”的冲动)

微软的架构师做了个非常聪明的选择:不干涉其他线程的内政。我不会生拉硬拽让你来处理我的消息,我会把消息投递给你(这个投递是内部操作,从外面看,这条消息应该一直被认为是发送过去的),然后—— 我等着。

这下,球踢到了目标线程那边。目标线程一点也不含糊,既然消息来到了我的队列里,那我的 GetMessage 会按照既定的流程走,不过,和上文WM_NCLBUTTONUP 的经历有所不同。鉴于这条消息是外来客,而且是Send 方式,于是它以优先于线程内部的其他消息进行处理(毕竟友邦在等着啊),处理完毕之后,把结果返回给消息的源线程。可以参见下文中对 GetMessage 函数的叙述。

在我们的现在讨论的这个例子里,由于 SendMessage(WM_SYSCOMMAND) 是属于本线程内的,所以就会递归调用回窗口的回调函数里。此后的处理,还是另外的几个消息被衍生出来,如 WM_CLOSE 和 WM_DESTROY。这个例子仅仅出于概念性的展示,而不是完全精确可靠的,而且,在 Windows Mobile 上,干脆就没有非客户区的概念。

这就是系统内所有消息的处理方式。

不过稍等,PostThreadMessage 投递到消息队列里的消息怎么办?答案是:你自己看着办。最好的处理位置,就是在消息循环中的TranslateMessage 调用之前。

另外一个需要稍做注解的问题是消息的返回值问题,这个问题有些微妙。对于大多数的消息,返回值都没有什么意义。对于另外的一些消息,返回值意义重大。我相信有很多人对 WM_ERASEBKGND 消息的返回值会有印象,该消息的返回值直接影响到系统是不是要进行缺省的绘制窗口背景操作。所以,处理完一条消息究竟应该返回什么,查一下文档会更稳妥一些。

这才算是功德圆满了。

2.3.      消息的优先级

上一节中其实已经暗示了这一点,来自于其他线程的发送的消息优先级会高一点点。

不过还需要注意,还有那么几个优先级比正常的消息低一点点的。它们是:WM_PAINT、WM_TIMER、WM_QUIT。只有在队列中没有其他消息的时候,这几个消息才会被处理,多个 WM_PAINT 消息还会被合并以提高效率(内幕揭示:WM_PAINT 其实也是一个标志位,所以看上去是被“合并了”)。

其他所有消息则以先进先出(FIFO)的方式被处理。

2.4.      没有处理的消息呢?

有人会问出这个问题的。事实上,这差不多就是一个伪命题,基本不存在没有处理的消息。从 4.2.2 节的叙述也可以看出,消息总会流到某一个处理分支里去。

那么,我本人倾向于提问者在问这样一个问题:如果窗口回调函数没有处理某个消息,那这个消息最终怎么样了?其实这还是取决于回调函数实现者的意志。如果你只是简单地返回,那事实上也是进行了处理,只不过,处理的方式是“什么都没做”而已;如果你把消息传递给 DefWindowProc,那么它会处理自己感兴趣的若干消息,对于别的消息,它也一概不管,直接返回。

3.       消息死锁

假设有线程A和B, 现在有以下步骤:

1) 线程A SendMessage 给线程B,A 等待消息在线程B 中处理后返回

2) 线程 B 收到了线程A 发来的消息,并进行处理,在处理过程中,B 也向线程 A SendMessage,然后等待从A 返回。

此时线程A正等待从线程B返回,无法处理B发来的消息,从而导致了线程A 和B相互等待,形成死锁。

以此类推,多个线程也可以形成环形死锁。

可以使用 SendNotifyMessage 或 SendMessageTimeout来避免出现此类死锁。

标题基于SpringBoot+Vue的学生交流互助平台研究AI更换标题第1章引言介绍学生交流互助平台的研究背景、意义、现状、方法与创新点。1.1研究背景与意义分析学生交流互助平台在当前教育环境下的需求及其重要性。1.2国内外研究现状综述国内外在学生交流互助平台方面的研究进展与实践应用。1.3研究方法与创新点概述本研究采用的方法论、技术路线及预期的创新成果。第2章相关理论阐述SpringBoot与Vue框架的理论基础及在学生交流互助平台中的应用。2.1SpringBoot框架概述介绍SpringBoot框架的核心思想、特点及优势。2.2Vue框架概述阐述Vue框架的基本原理、组件化开发思想及与前端的交互机制。2.3SpringBoot与Vue的整合应用探讨SpringBoot与Vue在学生交流互助平台中的整合方式及优势。第3章平台需求分析深入分析学生交流互助平台的功能需求、非功能需求及用户体验要求。3.1功能需求分析详细阐述平台的各项功能需求,如用户管理、信息交流、互助学习等。3.2非功能需求分析对平台的性能、安全性、可扩展性等非功能需求进行分析。3.3用户体验要求从用户角度出发,提出平台在易用性、美观性等方面的要求。第4章平台设计与实现具体描述学生交流互助平台的架构设计、功能实现及前后端交互细节。4.1平台架构设计给出平台的整体架构设计,包括前后端分离、微服务架构等思想的应用。4.2功能模块实现详细阐述各个功能模块的实现过程,如用户登录注册、信息发布与查看、在线交流等。4.3前后端交互细节介绍前后端数据交互的方式、接口设计及数据传输过程中的安全问题。第5章平台测试与优化对平台进行全面的测试,发现并解决潜在问题,同时进行优化以提高性能。5.1测试环境与方案介绍测试环境的搭建及所采用的测试方案,包括单元测试、集成测试等。5.2测试结果分析对测试结果进行详细分析,找出问题的根源并
内容概要:本文详细介绍了一个基于灰狼优化算法(GWO)优化的卷积双向长短期记忆神经网络(CNN-BiLSTM)融合注意力机制的多变量多步时间序列预测项目。该项目旨在解决传统时序预测方法难以捕捉非线性、复杂时序依赖关系的问题,通过融合CNN的空间特征提取、BiLSTM的时序建模能力及注意力机制的动态权重调节能力,实现对多变量多步时间序列的精准预测。项目不仅涵盖了数据预处理、模型构建与训练、性能评估,还包括了GUI界面的设计与实现。此外,文章还讨论了模型的部署、应用领域及其未来改进方向。 适合人群:具备一定编程基础,特别是对深度学习、时间序列预测及优化算法有一定了解的研发人员和数据科学家。 使用场景及目标:①用于智能电网负荷预测、金融市场多资产价格预测、环境气象多参数预报、智能制造设备状态监测与预测维护、交通流量预测与智慧交通管理、医疗健康多指标预测等领域;②提升多变量多步时间序列预测精度,优化资源调度和风险管控;③实现自动化超参数优化,降低人工调参成本,提高模型训练效率;④增强模型对复杂时序数据特征的学习能力,促进智能决策支持应用。 阅读建议:此资源不仅提供了详细的代码实现和模型架构解析,还深入探讨了模型优化和实际应用中的挑战与解决方案。因此,在学习过程中,建议结合理论与实践,逐步理解各个模块的功能和实现细节,并尝试在自己的项目中应用这些技术和方法。同时,注意数据预处理的重要性,合理设置模型参数与网络结构,控制多步预测误差传播,防范过拟合,规划计算资源与训练时间,关注模型的可解释性和透明度,以及持续更新与迭代模型,以适应数据分布的变化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值