Good Software Engineering Practices to Follow

Good Software Engineering Practices to Follow

如果在嵌入式系统上开发自动驾驶系统。这样的系统必须安全可靠、高效地工作。对低延迟、可靠性、故障恢复等都有要求。为了生产这样的软件产品,开发过程中都应当遵循某些好的软件工程原则。

GitLab-CI



Requirements

  • Latency Critical:与非嵌入式系统不同,嵌入式系统会实时响应来自环境的数据,并采取迅速的行动。我们需要绝对的保证,所有的计算任务必须在一个固定的时间窗口内完成。在我们的代码的任何部分都不允许有无限期的延迟。

  • High throughput:自动驾驶汽车从环境中接收到的传感器数据量和计算量的要求都非常高,我们希望能够增加更多的数据输入和计算量,以达到更高的安全目标。所有这些数据必须由一个系统资源有限的平台处理。因此,高数据吞吐率是另一个需求。

  • Functional Correct:任何计算机程序都需要产生正确的计算结果才能按设计工作。然而,许多模式识别技术,如计算机视觉和机器学习,大量应用于自动驾驶汽车。对于哪些被认为是功能性正确的,哪些不是,存在一些模糊。我们需要仔细定义概念,并确保软件代码产生的预期计算结果一致和可预测。

  • Failure resilience:任何失败都是可能的。我们的系统必须能够处理所有可能的故障情况,以确保乘客和车辆的安全。要做到这一点,我们需要了解所有可能失败的事情,并找出如何处理它们。

How software code or hardware can fail

  • 它没有进行正确的计算来产生预期的结果。
  • 它计算并产生预期的结果,但结果仍然不够好。
  • 它进行正确的计算并产生正确的结果,而且结果很好。但是计算时间太长了。
  • 它正确地完成了所有事情,并及时地交付结果,但却消耗了太多的系统资源,在其他地方造成了问题。

Before Write Code

优秀的软件工程不仅仅是编写正确有效的代码。
我们的工程师需要学习如何在编写代码时提出正确的问题。他们应该问:
1.特性的外部依赖关系是什么?
2.函数行为的可预测性是什么?
3.有什么是极端情况和未定义的行为吗?
4.函数的副作用是什么?它们是阻塞还是非阻塞?他们是否泄露了数据?它们是否受到来自其他线程或其他地方的泄漏的影响?
5.系统资源要求是什么?资源是否得到有效利用?
6.功能的复杂性是多少?一个普通的程序员能处理它吗?

Good Programming Practices

  • SIMPLIFY: 软件工程是关于问题分解的。我们把复杂的大问题分解成简单的小问题。我们一直这样做,直到碎片变得足够小,足够简单,我们可以理解和处理。所以保持简化。

  • Multi-threading: 系统必须有效地执行多个任务,因此需要多线程。但是多线程编程要复杂得多。因此,我们必须小心并遵循良好的编程实践。

  • Synchronous vs Asynchronous: 我们的大脑更容易理解什么时候任务需要等待数据才能继续,而在等待过程中CPU周期被浪费了。因此为了提高性能,我们更喜欢异步完成任务,并使用各种同步技术来允许多个线程协调它们的任务。

  • Blocking vs Non-Blocking. Atomic Operations. Lock-free programming: 我们的思维过程更习惯于顺序地思考事情,所以我们倾向于写代码来顺序地做事情,当数据或资源不能立即获得时阻塞。大多数人没有意识到的是,当函数调用阻塞时,它会对后续任务产生不良影响。因此,我们应该更倾向于编写非阻塞代码。每个执行路径都应该能够在有限的时间窗口完成。此外,在数据竞争或不太可能发生竞争的最简单情况下,我们更喜欢使用原子操作来保护数据完整性,而不是使用传统的互斥锁方法。即使可以立即获得互斥锁,对互斥锁的操作也会非常昂贵。鼓励人们探索无锁编程的主题。

  • Critical Path: 通过关键路径的任务的延迟很重要。阻塞代码不能存在于任何关键路径中。如何确保完成所有需要的计算,而不会在关键路径中创建阻塞代码?将阻塞计算移到单独的执行线程中,然后尝试使用同步对象在线程之间同步任务。

  • Synchronizations Objects: 允许不同线程相互通信并保持其任务一致和同步的数据对象。同步对象包括:读写锁、信号量、互斥锁、自旋锁、原子操作。对同步对象的操作可能会阻塞和死锁,可能导致阻塞甚至死锁的极端情况的数量可以呈指数增长。
    因此,在使用这些同步对象时,我们必须非常小心,将它们的使用最小化到绝对必要的程度,这样极端例子的数量就不会呈指数增长,并且保持在可管理的范围内。
    记录并证明为什么每次使用都是必要的,为什么它们不会造成阻塞或死锁问题。尤其要避免获取多锁,这会导致死锁。当代码被更改时,再次评估并记录和证明为什么新代码不引入阻塞或死锁的新情况。

  • System Resource: 要尽量减少系统资源的使用,并尽量减少持有资源的时间。唯一的例外是你希望几乎所有时间都持有的资源。分配和释放资源的时间成本可能很高,因此不妨一次性分配它们,然后永久持有。但即便如此,代码的不同部分需要最小化时间持有一种资源。(通过{}来管理生命周期)

Good Engineering Practices

  • 我们必须认识到自己的极限。我们的思维过程容易出错。即使是中等复杂的逻辑,我们正确处理的能力也非常有限。因此,我们不应该通过人肉发现错误或判断一段代码是否正确。
  • 用数据说话。我们唯一能确保方法就是测试它。我们不应该提交未经测试的代码。未经测试的代码就是未经验证的代码,因此是可能有bug的。
  • 简化单元测试,编写大量代码进行广泛的单元测试。理想情况下,单元测试应该是非常全面的,它应该涵盖我们所能想到的任何边界情况。这里的关键是创建一个好的框架,以便人们能够轻松地编写和运行单元测试。如果太难了,人们就不会去做。
  • 简化依赖关系。如果一个功能依赖于许多其他部分,那么就不太确定该功能是否每次都能一致工作。具有最小外部依赖关系的功能应该很容易测试,我们应该觉得为它编写单元测试很容易。如果我们不能进行单元测试,那么我们需要考虑如何简化代码。
  • 集成的整个系统回归测试应该是测试的最后手段。我们希望单元测试能够识别并修复几乎所有的代码错误。当我们把所有的工作部分放在一起时,期望是整个系统在试验中应该工作。

强烈推荐谷歌测试框架

兴趣使然