警告

您正在阅读的 ROS 2 文档版本已达到 EOL(生命周期结束),不再受官方支持。如果您想了解最新信息,请访问 Jazzy.

质量指南:确保代码质量

本页就如何提高 ROS 2 软件包的软件质量提供指导,与《ROS 2》的 "质量实践 "部分相比,本页侧重于更具体的领域。 开发人员指南.

以下各节将讨论 ROS 2 核心、应用和生态系统软件包以及核心客户端库(C++ 和 Python)。提出解决方案的动机是出于设计和实施方面的考虑,以提高 "可靠性"、"安全性"、"可维护性"、"确定性 "等与非功能性要求相关的质量属性。

静态代码分析是软件包构建的一部分

背景:

  • 您已开发出 C++ 生产代码。

  • 您已创建了一个支持构建的 ROS 2 软件包,其中包含 动情.

问题:

  • 库级静态代码分析不作为软件包构建程序的一部分运行。

  • 库级静态代码分析需要手动执行。

  • 在构建新软件包版本前忘记执行库级静态代码分析的风险。

解决方案:

  • 使用 动情 来执行静态代码分析,作为软件包构建程序的一部分。

实施情况:

  • 插入货包 CMakeLists.txt 锉刀

...
如果(构建测试)
  查找软件包(自动 要求)
  ament_lint_auto_find_test_dependencies()
  ... endif()
...
  • 插入 ament_lint 在软件包中加入测试依赖项 package.xml 锉刀

... <包装 格式="2";>;
  ...
  <test_depend>ament_lint_auto</test_depend>;
  <test_depend>ament_lint_common</test_depend>;
  ... </package>;

实例:

结果背景:

  • 支持的静态代码分析工具 动情 作为软件包构建的一部分运行。

  • 不支持静态代码分析工具 动情 需要单独执行。

通过代码注释进行静态线程安全分析

背景:

  • 您正在开发/调试多线程 C++ 生产代码

  • 您可以在 C++ 代码中通过多个线程访问数据

问题

  • 数据竞赛和死锁会导致严重的错误。

解决方案

实施背景:

要启用线程安全分析,必须对代码进行注释,让编译器了解代码的更多语义。这些注释是特定于 Clang 的属性,例如 __属性__(能力())).ROS 2 不直接使用这些属性,而是提供了预处理器宏,这些宏在使用其他编译器时会被删除。

这些宏可以在 rcpputils/thread_safety_annotations.h

螺纹安全分析文件指出

线程安全分析可与任何线程库一起使用,但要求线程应用程序接口封装在具有适当注解的类和方法中

我们决定让 ROS 2 开发人员能够使用 std:: 直接开发线程基元。我们不想像上面建议的那样提供自己的封装类型。

有三个 C++ 标准库值得注意 * GNU 标准库 libstdc++ - 默认情况下,可通过编译器选项 -stdlib=libstdc++ * LLVM 标准库 libc++ (又称 libcxx ) - macOS 默认,由编译器选项明确设置 -stdlib=libc++ * Windows C++ 标准库 - 与本用例无关

libcxx 标注其 std::mutexstd::lock_guard 线程安全分析的实现。当使用 GNU libstdc++ 因此,线程安全分析不能用于非包 std:: 类型

因此,要直接使用线程安全分析 std:: 类型,我们必须使用 libcxx

实施:

这里的代码迁移建议并不完整--在编写(或注释现有)线程代码时,我们鼓励你根据自己的用例合理利用尽可能多的注释。不过,这个分步步骤是一个很好的起点!

  • 对软件包/目标进行分析

    当 C++ 编译器为 Clang 时,启用 -线程安全 标志。基于 CMake 的项目示例如下

    如果(cmake_cxx_compiler_id 比赛 "Clang";)
      添加编译选项(-线程安全)   # 为你的整套服务
      目标编译选项(${我的目标} 公众 -线程安全)  # 针对单个库或可执行文件
    endif()
    
  • 法典注释

    • 第 1 步 - 注释数据成员

      • 在任何地方找到 std::mutex 用于保护某些成员数据

      • 添加 RCPPUTILS_TSA_GUARDED_BY(mutex_name) 标注到受互斥保护的数据上

       Foo {
      :
        空白 奖励(int 数量) {
          标准::锁定<;标准::储能器>; 水闸(贮存器);
          酒吧 += 数量;
        }
      
        空白 获取()  {
          返回 酒吧;
        }
      
      私人:
        可变 标准::储能器 贮存器;
        int 酒吧 rcpputils_tsa_guarded_by(贮存器) = 0;
      };
      
    • 步骤 2 - 修复警告

      • 在上述例子中 Foo::get 会产生编译器警告!要解决这个问题,请在返回条形码前锁定

      空白 获取()  {
        标准::锁定<;标准::储能器>; 水闸(贮存器);
        返回 酒吧;
      }
      
    • 步骤 3--(可选但建议)重构现有代码,使其符合私有-互斥模式

      线程 C++ 代码中的一个推荐模式是始终将您的 储能器 作为 私人 的成员。这使得数据安全成为包含结构的关注点,从结构用户那里卸下了责任,并最大限度地减少了受影响代码的表面积。

      要将锁私有化,可能需要重新考虑数据的接口。这是一个很好的练习,以下是一些需要考虑的问题

      • 您可能希望提供专门的接口,以执行需要复杂锁定逻辑的分析,例如计算互斥保护映射结构过滤集中的成员,而不是将底层结构实际返回给消费者

      • 在数据量较小的情况下,考虑复制以避免阻塞。这样可以让其他线程继续访问共享数据,从而提高整体性能。

    • 步骤 4--(可选)启用负能力分析

      https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#negative-capabilities

      负能力分析可让您指定 "调用此函数时不得持有此锁"。它可以揭示其他注释无法揭示的潜在死锁情况。

      • 您指定的位置 -线程安全,添加额外的标记 -线程安全-阴性

      • 在任何获取锁的函数上,使用 RCPPUTILS_TSA_REQUIRES(!mutex) 模式

  • 如何进行分析

    • ROS CI 构建农场每晚运行一项工作,其中包括 libcxx当线程安全分析提出警告时,它将被标记为 "不稳定",从而揭示 ROS 2 核心堆栈中的任何问题

    • 对于本地运行,您可以使用以下选项,所有选项都是等价的

      • 请使用 colcon clang-libcxx mixin

      • 将编译器传递给 CMake

        • 胶管 构建 --cmake-args -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS='-stdlib=libc++ -d_libcpp_enable_thread_safety_annotations'(启用线程安全注释)。 -dforce_build_vendor_pkg=on --no-warn-unused-cli

      • 覆盖系统编译器

        • CC=clang CXX=clang++ 胶管 构建 --cmake-args -DCMAKE_CXX_FLAGS='-stdlib=libc++ -d_libcpp_enable_thread_safety_annotations'(启用线程安全注释)。 -dforce_build_vendor_pkg=on --no-warn-unused-cli

结果背景:

  • 在编译时,当使用 Clang 和 libcxx

动态分析(数据竞赛 & 死锁)

背景:

  • 您正在开发/调试您的多线程 C++ 生产代码。

  • 您可以使用 pthreads 或 C++11 线程 + llvm libc++(如果使用 ThreadSanitizer)。

  • 不使用 Libc/libstdc++ 静态链接(在使用 ThreadSanitizer 时)。

  • 您不会构建与位置无关的可执行文件(在 ThreadSanitizer 的情况下)。

问题

  • 数据竞赛和死锁会导致严重的错误。

  • 使用静态分析无法检测到数据竞赛和死锁(原因:静态分析的局限性)。

  • 在开发调试/测试过程中不得出现数据竞赛和死锁(原因:通常没有对生产代码中所有可能的控制路径进行测试)。

解决方案

  • 使用侧重于查找数据竞赛和死锁的动态分析工具(此处为 clang ThreadSanitizer)。

实施:

结果背景:

  • 在部署前发现生产代码中的数据竞赛和死锁的几率更高。

  • 分析结果可能缺乏可靠性,工具还处于测试阶段(就 ThreadSanitizer 而言)。

  • 生产代码工具化造成的开销(为工具化/非工具化生产代码维护不同分支等)。

  • 每个线程需要更多内存(在使用 ThreadSanitizer 的情况下)。

  • 检测代码会映射大量虚拟地址空间(以 ThreadSanitizer 为例)。