问题: 从LabVIEW 2012开始正式推出操作者框架(Actor Framework),操作者框架一经推出,就在处理异步多进程调用及通信方面展现出无比的优势,但操作者框架在实际使用时还有许多问题需要注意,譬如,如果程序运行过程中某个进程出现了错误,那整个程序作为一个整体,该如何应对?
解答: 错误处理在软件工程中是一个非常庞杂的问题,本文将主要从以下几个方面展开。
1、操作者框架中的错误处理vi:
操作者框架的设计思想其实并不是什么新的东西,其实就是基于LVOOP对LabVIEW中传统的队列状态机进行的封装,从面向过程的角度看,传统的LabVIEW队列状态机程序框图可能如下:
操作者框架基于LVOOP对上述程序框图进行封装后可以得到如下的程序框图,基本的处理操作被Do.vi取代,错误处理被Handle Error.vi接管,此外,退出while循环后整个进程还有个Stop.vi。注意到这三个vi都是可用于重写的,通过重写这几个vi,可以很轻易地创建出一个实现特定任务的新进程。
打开Handle Error.vi可以看到操作者框架提供的默认错误处理vi行为是比较简单的,如下图:
如果基于操作者框架编写的进程具有自己的错误处理行为,可以新建自己的Handle Error.vi重写父类vi,并在自己的Handle Error.vi中填入对应的代码。
我们以出现错误代码1时不停止进程为例,对应的子类Handle Error.vi代码可以这样编写:
2、操作者框架中树形拓扑结构下的错误处理方案
一旦某个进程出现了运行错误,该进程可以首选自己来处理这个错误,但是绝大多数时候,该进程还需要将此错误信息发送到整个程序的其他进程中(譬如让高层进程知晓错误并采取必要的措施)。使用操作者框架编写大型的应用程序会得到如下图所示的常见的程序拓扑结构。不同层次的操作者可以根据需要在程序运行的不同阶段被调用,且自身被调用的同时也能选择继续调用嵌套层次更深的操作者,如此,便很容易形成树状结构。
在树状结构中操作者框架提供的API天然支持的进程间通信如下图所示,可以看到,通信链路是自下而上、从嵌套层次深的操作者向嵌套层次高的操作者传递的。同一嵌套层次的操作者间属平行关系,默认情形下无法相互通信。
使用Read Self Enqueuer.vi向进程自身发送错误信息常见于操作者多循环结构编程中,例如,某进程覆盖父类Actor Core.vi后自身的Actor Core.vi程序框图如下所示,这时很容易想到的一个问题是如果分立循环发生错误该如何处理?
通过使用Read Self Enqueuer.vi,即可将分立循环中发生的错误统一发送到Actor Core.vi处理,从而做到进程内多线程错误的集中处理。
使用Read Caller Enqueuer.vi向进程调用者发送错误信息目的是比较明显的,就是要把错误信息向顶层操作者传递,这种情况一般发生在该进程发生了严重的错误,超出自身处理能力,或者是发生的错误必须让高层操作者知晓,以改变整个程序的应对方式。不过在这里需要指出的是直接使用Read Caller Enqueuer.vi向进程调用者发送错误信息的方式实际用的比较少,操作者框架一般推荐使用处理最后操作的方式(Handle Last Ack)向调用者传递出错的进程及错误信息。
默认情况下,最后操作消息(Last Ack Msg)包含即将关闭的操作者信息及操作者要向调用者传递的错误簇,调用者在接到这个消息后会把包含的错误给解析出来并作为自身的错误输出。
由于在默认的操作者框架下,操纵者只要发生错误就会终止自身的运行,故一个底层的操作者因为错误终止的行为也会引发雪崩式的上层操作者相继退出,如下图所示:
注意,操作者发出的最后操作消息(Last Ack Msg)只会向自己的调用方传递,不会向自己调用的嵌套操作者及与自己同级别的嵌套操作者发送。但是,在绝大多数情况下高层的操作者退出即意味着整个程序关闭,遗留下一些嵌套层次较深的操作者在后台继续运行是没有什么意义的,故为了弥补操作者框架的不足,我们还需要一套下行对话机制,使顶层的操作者能顺利地与不同层次的嵌套操作者通信。借助这种下行通信机制,开发人员可以很轻易地实现诸如顶层操作者向嵌套操作者发布关闭命令、顶层操作者向嵌套操作者发出获取信息申请、顶层操作者向嵌套操作者部署新的数据及命令等功能。此外,还能从根本上避免深层次嵌套操作者的调用方因为错误关闭,而嵌套操作者却继续留在内存中执行的情况。
为了在程序层面实现这种下行通信机制,顶层操作者在调用嵌套操作者的同时可以注意将嵌套操作者的通信队列引用给收集起来,并在需要的情况下通过此通信队列向深层嵌套操作者传递命令。
这里我们以停止整个程序为例。当顶层操作者关闭时会执行对应的Sop Core.vi,注意到前面已经介绍过这个Stop Core.vi也是一个可重写vi,为了使顶层操作者退出时能顺便向其调用的嵌套操作者部署停止命令,我们可以重写顶层操作者的Stop Core.vi如下。
3.操作者框架中集中错误处理方案
在第二部分介绍了树形拓扑结构下的处理策略,基于这种错误处理思路,整个程序的错误处理链路就基本基于下图所示的模版,即大量错误都会在顶层操作者中被处理。
这种错误处理方式的好处是不破坏原程序各进程间的拓扑结构,基本采用操作者框架(Actor Framework)提供的原生API解决错误处理问题;当然,其不足之处也非常明显,如果程序调用层次结构非常深的话,很有可能会冗余编程,此外,如果某个错误并不由嵌套操作者本身或顶层操作者来处理,那该错误具体在程序的哪个层级被处理很难在第一时间被发现。为此,有必要考虑另一种错误处理思路:集中错误处理。
集中错误处理言下之意就是在应用程序已有的诸多进程之外再单开一个进程,专门用来处理程序中不同进程产生的错误。当应用程序中不同的进程都向错误处理进程发送自己的错误时,整个程序的拓扑结构就发生变化了,如下图。
采用集中错误处理的方法优势是显而易见的:错误处理操作者可以在不同的应用程序中复用,应用程序针对不同的错误处理者及处理方式也是显而易见,此外,一旦错误被捕捉到,能第一时间根据发送方在整个应用程序中进行错误定位。不过,在充分利用这些优势之前,还需要在软件编程方面注意一些原则:
1.确保单独的错误处理进程由顶层操作者去调用,因为调用操作者层次越高,也就越抽象,一般也越能代表应用程序,并方便错误处理进程收集其他嵌套操作者的错误,在重大错误发生时以极短的通信链路通知自己的调用方。
2.确保除错误处理进程外其他进程能直接获得错误处理进程的通信队列引用,实现错误信息的星形通信链路。避免错误信息经过几个操作者再传到错误处理进程,给错误定位增加困难。
基于以上两条原则,开发者完全可以直接基于操作者框架自己写一个错误处理操作者在不同的程序中复用,不过,有句古话说的好,叫“他山之石,可以攻玉”,在动手之前查一下是不是已经有成熟的模块通常能保证我们事半功倍!
在本篇文章中推荐大家使用的是结构化错误处理库,这个库目前已经跟随笔者南征北战多个项目,其稳定的错误捕捉能力及易用性深受笔者喜爱,特别是其具有高度复用性,非常易于集成进操作者框架直接使用。
这个库各个VI的注解及用法可以参见库中的pdf说明文档,在此不再赘述,主要的精力将基于笔者具体的项目向大家介绍如何在操作者框架下使用这个库。整个库的调用非常简单,只需要两步即可在大多数应用程序中实现对这个库的集成使用。
1.在整个应用程序正式启动出来之前调用Central Error Handler_DynRun.vi,基于异步调用技术在后台启动错误处理引擎。
2.在顶层应用程序及各个嵌套操作者错误处理(Handle Error.vi)中嵌入Error Handling.vi,实现对各个模块的错误捕捉。
为了避免每个操作者都新建一个错误处理vi(Handle Error.vi)并重复上述代码,这里推荐采用继承的方式来更好地把SEH lib集成到操作者框架中,如下图所示,通过在操作者框架提供的最原始的Actor.lvclass和用户自己设计的进程操作者间加入一个抽象层并定义错误处理行为,减少为每个进程错误处理进行重复编码。
完成了这两步操作后即可认为SEH lib已经顺利集成到了应用程序中并可以正常工作了了,但是,介于不同的应用程序有不同的错误处理策略,用户还可以选择设定特定错误处理表并自定义错误处理引擎。在SHE.lvlib: Error Handling.vi中一旦填写特定的错误处理数组后,在错误处理引擎中就可以自定义对此特定错误采取区别于一般错误的处理行为。
完成以上全部工作后,我们就基本实现了对我们的项目应用程序进行定制错误处理,如果应用程序在运行过程中发生了错误,那结构化错误处理引擎会在第一时间捕捉到,必要的时候还会跳出对话框提示用户出现了什么错误,错误定位源以及可能的原因!这些信息在多进程编程及调试过程中是至关重要的。
此外,所有发生的错误还会被记录在一个txt文件中(根据开发者的实际设置),如果一次运行发生的错误太多,开发者可以在调试源码时参考错误记录文件中的条目依次进行。
3.总结
本文介绍了操作者框架下的错误处理vi使用方法及两种典型进程拓扑结构下的错误处理方案,为了帮大家迅速在基于操作者框架编写的项目中定制错误处理方案,还在全文第三部分分享了结构化错误处理库的使用方法。全文从心法介绍到招式,希望对读者的项目编程能有所帮助。