嵌入式系统的架构设计——事件驱动架构的应用

2024/03/09

码意浓是一位软件开发人员,他最近心情不错,因为他将要负责一个关键的嵌入式系统软件开发。他决定拜访自己多年的老师,听听大师的意见。

大师:码意浓,好久不见,看你气色不错,有什么新鲜事想聊聊吗?

码意浓:嘿,大师,我正好有个新项目想请教您。我们打算开发一个新的嵌入式系统来替换那个已经服役快20年的老古董了。

大师:哦,终于要更新换代了,这可是个大好事。那个老系统确实已经跟不上时代了,维护起来也是头疼得很。

码意浓:是啊,说起来都是泪。不过我也有点担心,新系统的设计可不是件容易的事。

大师:别担心,我们一步一步来。你给我讲讲这个新系统。

码意浓:这个新系统主要是一个任务调度系统,它将在特定的嵌入式设备中运行,负责编排和执行各种任务。这些任务有的需要按计划执行,有的则是收到命令后立即执行。而且,这些任务都与网络IO有关,都需要去访问外部资源,获取数据后再进行加工处理。

大师:听起来确实是个复杂的系统。那在设计上,你们打算采用什么样的架构风格呢?

码意浓:这正是我头疼的地方。老系统采用的是多线程架构,虽然大家都很熟悉这种风格,但多线程带来的问题也不少,比如共享数据、死锁、问题复现困难以及调试难度大等。所以我在想,新系统是否应该继续采用多线程架构,还是有其他更好的选择?

大师:多线程确实有其局限性。不过,你也可以考虑一下事件驱动的架构风格。事件驱动在很多场景下都能提供更高的效率和更好的可扩展性。

码意浓:事件驱动?这听起来是个不错的选择。您能给我详细解释一下事件驱动的原理和优势吗?

大师:当然。简单来说,事件驱动就是基于事件的触发来执行任务。我们做个简单的比喻。多线程就像是高级饭店,每个顾客(任务)都有一个服务员(线程)专门为他们服务。客人来了有服务员点餐,从客人坐下到完成点菜,这个服务员都在那里等待。也就是阻塞式I/O。而事件驱动则像自助服务,没有服务员帮你点餐,你自己扫码点餐。系统会自动通知后厨。等饭菜好了,系统又会通知你自己去取餐。

码意浓:我明白了,事件驱动确实有很多优势。但是,如何实现事件驱动呢?有没有现成的模型可以参考?

大师:实现事件驱动的关键在于设计一个高效的事件循环机制。在这个机制中,系统会不断地监听事件源,一旦有事件发生,就触发相应的处理函数。这样,任务的执行就不再依赖于线程的切换,而是由事件的触发来驱动。至于现成的模型,你可以参考一下Reactor模型,它是一种常见的事件驱动模型,已经在很多系统中得到了成功的应用。

码意浓:听起来很高大上的样子。但是我这个系统似乎可以简单一些。这个系统运行在一个封装了MQTT的总线架构上,所以MQTT的消息可以作为事件。由这些事件可以驱动所有的任务执行过程。您觉得这样可行吗?

大师:哈哈,看来你已经有了自己的想法。确实,如果你的系统已经运行在一个封装了MQTT的总线架构上,那么MQTT的消息作为事件驱动任务的执行是完全可行的。这样一来,你就可以利用MQTT的发布/订阅模式来实现任务的调度和执行了。不过,事件驱动+多线程的方案确实比老系统的方案更加复杂,可能会对系统的稳定性产生不利影响。因此,在实现时你需要格外注意线程安全和并发控制的问题。

码意浓:您说得对。我也在担心这个问题。那么,有没有可能采用单线程来实现事件驱动呢?这样或许可以简化系统的复杂度。

大师:单线程实现事件驱动?这确实是个大胆的想法。不过,在某些场景下,单线程确实可以实现高效的并发处理。比如Node.js就是一个典型的例子,它采用单线程异步I/O的模型,实现了高并发的处理能力。但是,这需要你的系统具备异步化的特点,所有的I/O操作都是非阻塞的,这样才能充分利用单线程的优势。

码意浓:您说得没错。其实我们的系统已经具备了异步化的特点。异步调用的成本大概在10ms以内。作为边缘设备,我们的并发量并没有服务器那么高,所以10ms以内的成本是完全可以接受的。这样一来,性能应该不会成为问题吧?

大师:哈哈,看来你已经考虑得很周到了。确实,如果异步调用的成本在可接受的范围内,那么单线程实现事件驱动是完全可行的。而且,事件驱动还能带来一个额外的好处,就是解耦。由于事件的触发和处理是分离的,所以你可以很方便地添加、删除或修改事件处理函数,而不会影响到其他部分的代码。这对于系统的可扩展性和可维护性都是非常有利的。

码意浓:太好了!解耦确实是我们这个系统非常需要的一个特性。因为全国各省的需求是有差异的,新系统需要在一套系统上实现全国各省的需求。这确实是个很大的业务挑战。您有什么建议吗?

大师:针对这个业务挑战,我建议你在设计系统时充分考虑关注点分离、单一职责、开闭原则以及Unix的机制与策略分离原则等设计原则。通过将系统划分为多个独立的组件,每个组件负责一个特定的功能或业务逻辑。如果这些组件能够被扩展、被替换、被编排,你可以实现高度的可扩展性和可配置性。这样,你就可以根据各省的具体需求来定制和扩展相应的组件了。同时,事件驱动的架构风格也能帮助你实现组件之间的解耦和通信。

码意浓:您的建议非常中肯!回去后我会好好思考这些原则,并尝试将它们应用到我们的系统设计中。谢谢您!

码意浓回到家中,依然非常兴奋,脑中都在想事件驱动、单线程、可扩展、可替换、可编排!

这天晚上,他梦到了圆圆的月亮,月亮挂在天空,也映在地面上的多条河流中。

river2

第二天,码意浓又来拜访大师。

码意浓:大师,我又来了。经过昨天的讨论,我对新系统的设计有了更清晰的认识。我想跟您分享一下我的设想:这个任务调度系统,就像一个自然生态,有多条河流,每条河流都跑着任务,对应并行运行的任务流。在每条河流中,任务是串行流动(执行)的,而且是按优先级排序流动。您觉得这样的设计怎么样?

大师:哈哈,你的设想很有创意!将任务调度系统比作河流,确实能够形象地描述任务的流动和执行过程。

码意浓:是的。每个任务由多个原子任务组成。原子任务是最小的任务,不可拆分。原子任务可以被扩展或者替换,任务则可以对原子任务进行编排。这样就可以实现一套标准实现,然后各省的需求可以在这个基础上,实现扩展、或者替换掉某些原子任务,也可以对原子任务进行重新编排。最后形成一个原子任务库,根据各省需求将原子任务编排成各种任务,这些任务按不同的优先级运行在多条河流中。

大师:原子能力和编排这个思路非常好,系统将具有强大的可扩展性。再加上事件驱动带来的解耦能力,它肯定能够满足各种需求差异!

码意浓:大师的组件可扩展、可替换和可编排启发了我。有了这三个特性,我们的架构可以应对任何挑战。

大师:很好!不过,我还有一个问题。如果任务正在执行时,突然来了一个更高优先级的任务,你应该怎么处理呢?

码意浓:我的想法是,将原子任务作为最小的调度单位。当高优先级的任务到来时,如果当前原子任务还没有执行完,那么就让它继续执行完。然后,在当前原子任务执行完毕后,立即插入这个高优先级的任务进行执行。这样,原子任务既是设计的最小业务组件,也是运行时的最小业务组件。

大师:嗯,你的处理方式很合理。将原子任务作为最小的调度单位确实是与你的业务特点匹配的,而且能够简化任务的管理和调度过程。另外,你的设计还考虑到了任务的优先级和插入时机的问题,这在实际应用中也是非常重要的。最后它们还提供了系统所需的扩展性,能够满足全国各省业务的差异化需求。而这些还是建立在单线程基础上,对开发调试来说方便太多了。总的来说,你的设计思路很清晰,也很有创意。我相信按照这样的设计去实现新系统的话,一定能够取得很好的效果!

码意浓:谢谢您的肯定!有了您的指导和建议,我对新系统的设计更有信心了。我很期待把这个系统建好,为我们的产品打下坚实基础!

大师:祝你成功!

当码意浓离开的时候,他的内心充满了激动,眼神中透露出无比的坚定。愿我们共同为他送上祝福!