王靖WANG Jing;杨成YANG Cheng
(①北京信息职业技术学院信息工程系,北京 100018;②北京星网锐捷网络技术有限公司,北京 100082)
(①Department of Information Engineering,Beijing Information Teconology College,Beijing 100018,China;②Beijing Star-net Communication Co.,Ltd.,Beijing 100082,China)
在现代应用程序中,为了用户界面的更加友好,程序运行的更加流畅,使用多线程进行任务的处理已经是主流的选择。但是多线程程序开发中,我们往往会遇到以下问题:
①在访问共享的数据时,使用加锁方式实现。但这种方式在程序规模增长到一定程度后,会不可避免地带来程序的低效、死锁等问题。②简单地为每个耗时的任务创建一个线程;这种模式首先造成资源的浪费,其次当大量线程被创建出后,会耗尽系统的资源从而导致系统变慢或死机;即使是正常关闭,大量线程在关闭时的等待也将是一个漫长的过程。③任务执行完后,通过回调通知发起者任务,但可能发起者已被销毁,从而导致程序的崩溃。
采用线程模型,及约定开发人员以事先制定的模式工作,能避免上述问题。线程模型的设计目标概括为以下几点:
①除了在线程模型的管理和调度模块,整个应用程序避免使用任何形式的锁。这样能避免程序低效、死锁等问题。②线程模型应提供能动态调整的线程池来执行用户提交的任务。③线程模型应提供统一的接口让用户提交任务、设定定时任务、设定任务过期条件、取消任务、存储任务和提供回调等,避免在每个模块做同样的工作。④合理调度任务,保证共享数据的安全。⑤线程模型应管理每个服务和回调的生命周期,使应用
程序避免崩溃。
如图1 所示,用户任务通过线程模型接口提交,提交过程是异步的,可立即返回;任务通过线程模型接口,添加到服务存储和管理队列;任务分派程序会选择合适的任务提交到线程池中,执行用户的任务,完成时调用回调函数;通过生命周期管理,当回调的对象销毁时,自动取消回调。
图1 线程模型图示
一个完整的线程模型至少应包含以下组成部分:
①线程模型接口;②任务存储和调度线程的管理;③任务的分派;④线程池的管理;⑤线程模型的辅助设施。
1.1.1 创建线程模型管理 线程管理模块在主程序入口处被创建。确保用户的各个模块都能调用到线程模型的各种接口。
1.1.2 销毁线程模型管理 线程管理模块在主程序出口处被销毁。销毁时,线程管理模块保证正在被执行的任务执行完,同时取消正在队列中等待的所有任务。
1.1.3 添加一个新的任务 线程模型提供辅助设施来协助用户创建任务。任务将有6 个属性:command,callback,task_id,group_id,priority 和timeout。
1.1.4 设定任务分组 如果有些任务需要访问共享的数据,这些任务将按顺序被放入线程池执行,避免多线程同时访问共享数据。通过给这些任务赋予相同的ID,任务分派模块就会自动的将相同ID 的任务顺序放入线程池。
1.1.5 设定任务回调 任务回调在任务执行完成后被工作线程调用,回调过程如需线程切换,可通过线程模型的辅助设施来实施。
1.1.6 设置任务优先级 每项任务都有自己的优先级,高优先级的任务将优先被放进线程池执行。
1.1.7 任务超时 有的任务会有一个执行的期限,如超过这个期限,回调函数将被执行。
1.1.8 取消任务 用户发起任务后,在等待任务执行的过程中,可能需要取消任务。此时,如任务在等待的队列中,任务将被删除。如任务已在线程池中被执行,则断开该任务连接的回调函数。
1.1.9 重试任务 在某个任务失败后,回调函数会通知用户此次执行失败和失败的原因。用户可通过重试该项任务,重新将任务发送到存储队列中等待执行。
1.1.10 预约任务 用户希望在一段时间后启动某些项任务,需要接口支持预约任务。
1.2.1 任务及其回调的存储 采用多索引容器(boost::multi_index_container)形式来存储任务,同时按照task id,priority 和group id 为任务建立不同的索引。这样,不但在查找相应任务时效率更高,而且也保证了插入或者删除数据时候的效率。[1]
1.2.2 任务管理及回调 ①由于用户可能在任意的线程调用线程模型,来添加希望的服务。为避免本文开始提到的对共享数据加锁的问题,需将任务的添加工作切换到任务管理线程执行。②维护用户任务的状态,Scheduling,Pending or Processing 也需在管理线程进行。③在回调发生的时候,需移除相应任务并触发用户预先设定的回调。回调必须在管理线程中执行,需检查管理线程中该任务是否被取消。④用户可能不断添加新任务,线程池会添加任务完成的事件到管理线程。同时,用户可能会取消之前添加的任务。以上操作会影响到共同数据,因此必须按顺序执行。但这样会导致大量添加新任务的操作,导致分派任务一直无法得到执行;在这种情况下,管理线程一直处于忙碌状态,但是线程池却处于空闲状态。因此,对于不同的任务的添加,也需设定优先级。一般来说,完成任务的优先级设为Medium,用户取消任务的优先级设为High,而添加任务的优先级设定为Low。
1.2.3 防止任务无限制占据线程 对线程模型来说,用户创建的任务是不可控的。因此,会发生由于用户任务错误导致线程池的线程进入死循环,使得线程丧失继续服务的能力。线程调度管理程序如不能及时发现死去的线程,将有可能导致线程池所有线程被占用,从而导致用户所有任务均无法执行。一般可以记录上次该线程回调发生的时间。如超过指定时间范围而无响应,可强制该线程关闭后重启或者关闭相应任务,并重新添加线程到线程池。
1.2.4 定时器组件 为实现用户预约任务,必须实现Timeout 部件,并在到期时,将回调的执行过程控制在管理线程中。实践中,可考虑用Boost::asio::deadline_timer。
1.2.5 内存池的管理 当等待任务多时,增加线程池中线程的数量。当等待队列很少或为空时,减少线程数量。增加减少不宜太频繁。一般根据一段时间内处理的任务数来决定开启的线程数。
1.2.6 任务回调的生命周期管理 对于回调任务,一般需做两件事。第一,确保回调发生在指定线程。这一点,1.4 节将会专门讲述。第二,确保回调所依赖的对象存在;如所依赖的对象已被销毁,那么就取消该回调。实现可采用boost::signal 模式,只要求回调所依赖的目标对象从boost::signals::trackable[2]派生即可。
①分派单元的运行需确保在任务管理线程中执行。②分派单元按优先级取任务,放入队列中执行。③如果标记为某个group id 的任务已在线程池中运行,那么该任务结束前,同样group id 的任务不能被再次放入。④添加任务时和complete task 时均可尝试重新分派任务。
①启动指定数目的线程。②任务能够通过接口添加到线程池的队列中。③运行时动态增减线程数量。④退出时确保运行中的任务执行完毕。
①创建任务。②创建回调命令。③提供Factory 机制,使目标线程可以注册相应的命令到Factory。该命令可将任意命令切换到线程执行。
线程模型执行的过程如图2 所示。
图2 线程模型执行过程
线程模型的使用者通过接口创建线程模型并拿到需要的接口。通过线程模型提供的辅助函数生成任务后,调用线程模型接口,把任务添加到线程模型管理的任务队列。管理线程,在任务队列不为空时,选择合适的任务,并将完成任务的事件和任务命令绑定。将组装好的命令放入线程池中去运行。执行完毕后,完成任务的事件被触发,并切换到管理线程。该事件将进行下一轮任务分派。
对于不同的应用场合,线程模型有着不同的优化策略。优化策略一般考虑的环节有:
①是否充分利用每个线程的执行能力。②是否最大限度地减少了任务在线程之间的调度。③有些任务只读共享的数据,有些需写那些数据。如果能将读写任务区分对待,那么读数据的任务就可以同时添加到线程池中。④调度管理程序处理添加和完成任务的优先顺序,及任务的存储结构。⑤任务队列的动态规划。
无论在客户端UI 编程,还是在服务端编程,线程模型都是一个非常重要的设施,能提高程序的稳定性和可维护性。对于规模较大的系统,这是一项非常重要的基础设施。本文结合在工程中的实践经验,详尽分析了设计一个线程模型时需考虑的目标、结构、接口及模型的工作流。实践中,这种线程模型能帮助应用程序简化设计,提高稳定性,提升效率。
[1]王凤岭.分布式操作系统中线程包实现方法的对比研究[J].南宁职业技术学院学报,2004(04).
[2]陈矫阳,陈楸,刘桓龙.基于LabWindows/CVI 多线程数据采集的研究[J].科学技术与工程,2008(09).
[3]周仕祥,刘伯恕.Boost 功率因数校正器的效率和空载损耗研究[J].电力电子技术,2003(03).
[4]肖和平,韩伟红,贾焰,吴泉源.StarCCM2.0 中高性能线程池模型的研究与实现[J].计算机工程,2005(24).