基于“管家模型”的异步消息通知与任务调度机制
2017-12-25汪利虎张凡石
◆汪利虎 张凡石 金 京
(1.中国人民解放军31011部队 北京 100091;2.中国电科集团28所 江苏 210007)
基于“管家模型”的异步消息通知与任务调度机制
◆汪利虎1张凡石2金 京1
(1.中国人民解放军31011部队 北京 100091;2.中国电科集团28所 江苏 210007)
本文提出了基于“管家模型”的异步消息通知与任务调度机制,主要用于在包含大量计算和业务相关性的应用系统中通过异步消息的方式简化随机任务调度与处理。通过在安卓操作系统上通过充分利用系统特性进行了轻量级的实现,验证了此模型的可行性及给开发工作带来的便捷性。
异步;多线程
0 引言
为了应对软件用户日益复杂的功能及体验需求,应用系统的设计正面临着越来越多的挑战,多任务的管理尤为突出。在现代操作系统已经能够非常好的支持多任务的背景下,如何充分利用其“多核”等特性,开发出符合客户复杂需求但足够稳定和健壮的应用,一直是软件业界的热门研究课题。
通常,实现多任务的方式有多进程与多线程两种,表1列出了这两种方式在数据共享、同步、资源占用、生命周期管理和开发调试等方面的对比情况。
表1 多进程与多线程实现方式对比情况
在包含大量计算和业务相关性的应用系统中进行随机任务的调度与处理,意味着较多的任务执行模块可能被毫无规律的、突发性的或频繁的创建与销毁,他们的生命周期可能很短暂,并且这些任务执行模块需要和应用程序的主模块通信以传输任务处理结果或告知其状态。在此背景下,本文提出了基于“管家模型”的异步消息通知与任务调度机制。
1 管家模型
1.1 整体架构与原理
为简化随机任务的调度处理,并解决软件系统开发中的线程管理和维护较易混乱的情况,本文借鉴生活中主人把事务交由管家代理并由各个下属仆人去执行的模式,将其应用到多线程的管理中来,并以此建立了“管家模型”。模型的整体架构如图1所示。
在大部分的应用系统与开发框架中(如Microsoft .NET框架和Android操作系统),对用户操作的响应和处理是通过事件驱动的。UI线程通常作为应用的主线程自系统启动后就一直运行并消费(Producer/Consumer模式)运行时的各种事件,如:鼠标点击(OnClick)事件,触屏(OnTouch)事件等。如果主线程(UI线程)执行过多的与UI渲染无关的操作和任务,可能导致UI无法响应用户与其的交互(ANR,Application Not Responding),为解决这个问题,必须将一些耗时费力的操作交由子线程处理,并且鉴于方便管理和统筹规划方面的考虑,在所有的子线程中设置两种角色:调度线程一个(管家)及工作线程若干(仆人)。主线程不需向各个子线程交办具体工作任务,而是通过调度线程下达命令,为使任务结果能够更快回传,各个子线程直接向主线程汇报任务处理情况,而他们的执行状态、生命周期等主线程不关心的信息则汇报给调度线程并由其管理和维护。
图1 “管家模型”的整体架构
1.2 模块分析
(1)主线程
当一个应用程序启动时,操作系统将会为其创建一个进程,同时一个线程也会立即运行在此进程中,该线程就是程序的主线程,主线程通常就是创建GUI的线程(UI线程)。因此,主线程是整个应用程序运行最主要的模块,执行程序的主要逻辑,负责绘制UI呈现给用户并且直接或间接处理所有与用户的交互。
在事件驱动的系统中,主线程自创建并运行后,会自动拥有一个消息循环及与其绑定的消息队列以存储未能及时处理的事件,消息队列中的事件将会被消息循环迅速、顺序的分发和处理。而对不同类型的事件做出相应处理以减轻主线程负担,加快其消息分发速度,避免其长时间无响应,这正是调度线程和工作线程存在的意义。
由于一些任务的行为是固化的,他们独立于具体的使用场景,因此主线程可以直接通过一些预先定义的命令字向调度线程发送这些固定的任务,这些命令字可举例如下:
①TASK_INIT:启动和初始化调度管理模块并将初始化后续的工作交由其进一步处理;
②TASK_STOP:结束调度管理模块的生命周期,并将善后处理工作交由其进一步处理;
③TASK_RESTART:由于某些原因造成调度管理模块工作异常,需要重新启动并初始化。
其余具体的任务将会以任务包和与其绑定标识的形式发送给调度线程,当主线程收到工作线程直接发来的任务结果时,可以按照之前的任务标识识别他们,并且做出相应反应,如更新UI元素或显示一个短暂提示等等。
(2)调度线程
调度线程在主线程与工作线程之间扮演着协调者或代理的角色:代理所有来自主线程的任务,维护所有的工作线程,协调他们之间的通信。主线程只需要向调度线程发送预先定义好的命令字或发起一个带有字符串形式标识的任务包,此任务包将会被预处理和进一步封装,然后根据当前维护的工作线程状态,随机选择一个空闲线程接收和执行此任务包。
与主线程类似,调度线程也会在自己内部维护一个消息队列(异步)以接收来自主线程及工作线程的各种消息,同时一个消息循环也会在线程启动后被绑定以便从消息队列中获取和分发消息,通常可以在此消息循环内部定义一个接口用来方便外部与其交互,如发送消息和定义消息处理方法等。需要注意的是,线程的启动可以根据实际情况选择“懒加载”(Lazy Init)的启动模式,即在应用程序第一次需要进行多任务处理时才启动此调度线程及相关的工作线程,这样处理的好处是可以避免计算资源的空置浪费,而计算资源一旦被启用后便可以复用以提高利用效率。此外,在调度线程内部创建一个调度器,将不同的线程调度策略和机制统一管理、方便维护,甚至在应用程序运行期间,可根据上下文环境实时置换策略(Strategy Pattern)。
(3)工作线程
工作线程在整个架构模型中负责具体的任务执行。这些线程资源被放入一个线程组中供管家按需调度,每个工作线程的生命周期由调度线程管理,并且根据调度线程发送的固定命令字或任务包做出响应,其运行状态的变化将及时通知上级(Status Update),即调度线程,如“忙->空闲”。如果工作线程被分配了具体任务,任务执行结束的结果直接上报给任务发送源,即主线程,以避免不必要的信息中转。
通常,工作线程组中线程的数量是一个实践经验参数,在不同的生产环境和框架下会有不同的配置,并且这些配置项都可以根据工程实际需要修改。例如,在Web应用服务器Tomcat中,默认的最小活跃线程数量参数minSpareThreads为25;在Java IoC框架Spring中,默认的核心线程数量参数corePoolSize为1。
2 实现及分析
Android是一种基于Linux的开源操作系统,是当今全球市场份额最大的移动终端操作系统。移动应用程序在运行期间经常需要处理用户发起的短小随机的计算任务,如从网络下载多个图片,从本地数据库读取大量数据并解析等。可见,管家模型非常适用于此类应用程序与用户的交互场景,事实上管家模型的思想也是受此启发而产生。
2.1 核心元素及与模型的对应关系
(1)Message:消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,终由Handler处理。映射为管家模型中的异步消息Msg。
(2)Handler:处理者,负责Message的发送及处理。使用Handler时,需要实现handleMessage(Message msg)方法来对特定的Message进行处理,例如更新UI等。Handler会引用当前线程里的Looper和MessageQueue,同一线程可以有多个Handler,并且他们都共享同一Looper和MessageQueue。
(3)MessageQueue:消息队列,每个线程只有一个,用来存放Handler发送过来的消息,并按照FIFO规则执行。当然,存放Message并非实际意义的保存,而是将Message以链表的方式串联起来的,等待Looper的抽取。映射为管家模型中的消息队列。
(4)Looper:消息泵,每个线程只有一个,不断地从MessageQueue中抽取Message执行。因此,MessageQueue与Looper一一对应。映射为管家模型中的消息循环。
(5)Thread:线程,负责调度整个消息循环,即消息循环的执行场所。
(6)HandlerThread: Thread类的增强,内部封装了消息循环供创建Handler以方便的进行消息发送和处理。映射为管家模型中的调度线程和工作线程。
(7)MasterHandler: 绑定到调度线程的 Handler,对来自主线程和工作线程的不同类型消息作出相应处理,包括进步封装和转发,更新内部维护数据的状态,定期进行状态报告等。
2.2 核心代码及消息和任务调度机制
如图2所示,将上述的管家模型映射为一种示例实现并作出了相应分析。(鉴于篇幅原因,此处只列出核心代码片段,其余代码已被删除)
图2 代码清单1
图3 代码清单2
如图 2所示,整个管家模型的核心功能被集中到MultiTaskManager类中,此类被实现为单例模式以保证全局唯一且一致的调用,并且在MultiTaskManager中保存了用以维护各工作线程状态的 threadStateMap,参与整个模型中消息发送和处理handler的 handlerMap,以及来自主线程的各种类型任务的taskMap。
图4 代码清单3
图5 代码清单4
如图3所示,startTask()方法展示了通过一个方法调用并传递异步消息、返回消息处理handler和具体任务(通过一个Runnable接口实现)的方式,在全局范围内调用MultiTaskManager以完成多任务调度,任务发送方只需向外指派任务和处理任务结果,而不需要关心底层诸如线程创建和销毁、计算资源的复用、数据同步等等一系列细节。
如图4所示,展示了调度线程的消息处理机制中较为重要的几个消息类型的处理方式:
(1)针对状态报告STATE_BUSY,STATE_IDLE,更新内部维护的数据状态并继续其他操作;
(2)针对初始化指令 TASK_INIT,将指令进步传递给工作线程组以完成整个系统初始化过程;
(3)针对任务调度指令 TASK_SCHEDULE,根据自身状态和可用资源情况执行调度操作。
如图5所示,展示了任务如何调度的一种简要操作方式,其关键步骤如下:
(1)获取当前空闲的计算资源,即工作线程,若失败,采用特定退避算法进行重新调度;
(2)封装任务行为,工作线程在执行任务之前(不论任务是否为空)将总是首先更新自身状态,并报告调度线程其状态更新;
(3)若任务不为空,执行任务;
(4)封装任务行为,工作线程在执行任务完毕后更新自身状态并报告调度线程其状态更新;
(5)直接向任务发送源(通常就是主线程)传递任务结果。
图6 “管理模型”在Android系统下实现的具体运行过程
图6通过一个从数据库加载大量数据的实例工程展示了上述管家模型在 Android系统下实现的具体运行过程,包含了从 UI线程发起的多任务管理模块的全局初始化、计算资源获取、任务调度与执行和返回任务结果等过程。由代码清单及图2可见,多任务管理模块的使用非常简洁,流程清晰。
3 结语
在这种“管家模型”中,系统中的不同角色通过规范化的异步消息(可统一抽象为消息标识和消息内容)进行通信,彼此之间独立运行,各自维护内部的逻辑和状态,因此系统的整体稳定性大幅提高,不会因某个子模块崩溃而导致整个系统崩溃,这种模型的设计更符合软件工程多年实践以形成的思想:“高内聚,松耦合”。
[1]阮晓星,杨亮,魏晋鹏.基于Intranet的事务处理系统[J].微计算机应用,1997.
[2]唐丽,赵强.基于CORBA实现分布式系统间的异步消息通信[J].计算机应用,2002.
[3]高鹏飞,李新明,孙建.Linux与VxWorks任务调度机制分析[J].工业控制计算机,2005.
[4]http://tomcat.apache.org/.
[5]http://spring.io/.