质量指南:确保代码质量

本页提供有关如何提高 ROS 2 软件包软件质量的指导,重点关注比 开发者指南 的质量实践部分更具体的领域。

以下部分旨在解决 ROS 2 核心、应用程序和生态系统软件包以及核心客户端库 C++ 和 Python。 所提出的解决方案是出于设计和实施考虑,旨在提高与非功能性需求相关的质量属性,如“可靠性”、“安全性”、“可维护性”、“确定性”等。

静态代码分析作为 ament 软件包构建的一部分

上下文:

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

  • 您已经使用 ament 创建了一个具有构建支持的 ROS 2 软件包。

问题:

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

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

  • 在构建新软件包版本之前,可能会忘记执行库级静态代码分析。

解决方案

  • 使用“ament”的集成功能将静态代码分析作为软件包构建过程的一部分来执行。

实施

  • 将“CMakeLists.txt”文件插入软件包。

...
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  ament_lint_auto_find_test_dependencies()
  ...
endif()
...
  • 将“ament_lint”测试依赖项插入到包的“package.xml”文件中。

...
<package format="2">
  ...
  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>
  ...
</package>

Examples:

Resulting context:

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

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

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

上下文:

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

  • 您在 C++ 代码中从多个线程访问数据

问题:

  • 数据争用和死锁可能导致严重错误。

Solution:

实施上下文:

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

这些宏可以在 rcpputils/thread_safety_annotations.hpp 中找到

线程安全分析文档指出 线程安全分析可以与任何线程库一起使用,但它确实要求将线程 API 包装在具有适当注释的类和方法中

我们已决定希望 ROS 2 开发人员能够直接使用 std:: 线程原语进行开发。 我们不想提供我们自己的包装类型,如上所述。

有三个 C++ 标准库需要注意

  • GNU 标准库 libstdc++ - Linux 上的默认库,通过编译器选项 -stdlib=libstdc++ 明确设置

  • LLVM 标准库 libc++``(也称为 ``libcxx ) - macOS 上的默认库,通过编译器选项 -stdlib=libc++ 明确设置

  • Windows C++ 标准库 - 与此用例无关

libcxx 注释了其 std::mutexstd::lock_guard 实现以进行线程安全分析。使用 GNU libstdc++ 时,这些注释不存在,因此线程安全分析不能用于非包装的 std:: 类型。

*因此,要直接将线程安全分析与*``std::`` *类型一起使用,我们必须使用*``libcxx``

实施:

此处的代码迁移建议绝不完整 - 在编写(或注释现有)线程代码时,我们鼓励您尽可能多地使用注释,只要这些注释符合您的用例的逻辑。 但是,这个分步指南是一个很好的起点!

  • 为包/目标启用分析

当 C++ 编译器为 Clang 时,启用``-Wthread-safety`` 标志。以下是基于 CMake 的项目的示例

if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wthread-safety)   # for your whole package
  target_compile_options(${MY_TARGET} PUBLIC -Wthread-safety)  # for a single library or executable
endif()
  • 注释代码

  • 步骤 1 - 注释数据成员

  • 找到使用 std::mutex 保护某些成员数据的任何位置

  • 为受互斥锁保护的数据添加 RCPPUTILS_TSA_GUARDED_BY(mutex_name) 注释

    class Foo {
    public:
      void incr(int amount) {
        std::lock_guard<std::mutex> lock(mutex_);
        bar += amount;
      }
    
      void get() const {
        return bar;
      }
    
    private:
      mutable std::mutex mutex_;
      int bar RCPPUTILS_TSA_GUARDED_BY(mutex_) = 0;
    };
    
  • 第 2 步 - 修复警告

  • 在上面的例子中 - Foo::get 将产生编译器警告!要修复它,请在返回 bar 之前锁定

    void get() const {
      std::lock_guard<std::mutex> lock(mutex_);
      return bar;
    }
    
  • 步骤 3 -(可选但推荐)将现有代码重构为私有互斥锁模式

线程 C++ 代码中的推荐模式是始终将“互斥锁”保留为数据结构的“私有:”成员。这使数据安全成为包含结构的关注点,从而将责任从结构用户身上转移,并最大限度地减少受影响代码的表面积。

将锁设为私有可能需要重新考虑数据的接口。这是一项很好的练习 - 以下是一些需要考虑的事项

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

  • 考虑复制以避免阻塞,因为数据量很少。这可以让其他线程继续访问共享数据,从而可能带来更好的整体性能。

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

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

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

  • 在指定 -Wthread-safety 的位置,添加附加标志 -Wthread-safety-negative

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

  • 如何运行分析

  • ROS CI 构建农场使用“libcxx”运行夜间作业,当线程安全分析发出警告时,它将通过标记为“不稳定”来显示 ROS 2 核心堆栈中的任何问题

  • 对于本地运行,您有以下选项,所有选项都等效

    • 使用 colcon clang-libcxx mixin (see the documentation for configuring mixins)

      colcon build --mixin clang-libcxx
      
    • 将编译器传递给 CMake

      colcon build --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++ colcon build --cmake-args -DCMAKE_CXX_FLAGS='-stdlib=libc++ -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS' -DFORCE_BUILD_VENDOR_PKG=ON --no-warn-unused-cli
      

Resulting Context:

  • 使用 Clang 和“libcxx”时,潜在的死锁和竞争条件将在编译时浮现出来

动态分析(数据竞争和死锁)

上下文:

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

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

  • 您不使用 Libc/libstdc++ 静态链接(如果使用 ThreadSanitizer)。

  • 您不构建非位置独立的可执行文件(如果使用 ThreadSanitizer)。

问题:

  • 数据竞争和死锁可能导致严重错误。

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

  • 数据竞争和死锁不得在开发调试/测试期间出现(原因:通常不会执行通过生产代码的所有可能的控制路径)。

解决方案:

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

实施:

结果上下文:

  • 在部署生产代码之前,更有可能在其中发现数据竞争和死锁。

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

  • 由于生产代码检测而产生的开销(为已检测/未检测的生产代码维护单独的分支等)。

  • 已检测的代码需要每个线程更多的内存(对于 ThreadSanitizer)。

  • 已检测的代码映射大量虚拟地址空间(对于 ThreadSanitizer)。