质量指南:确保代码质量

本页面提供关于如何改善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>

示例:

产生的上下文

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

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

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

上下文:

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

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

问题:

  • 数据竞争和死锁可能导致关键错误。

解决方案:

实现的上下文:

要启用线程安全分析,必须对代码进行注释,以便编译器了解代码的语义。这些注释是特定于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++代码中,推荐的模式是将 mutex 作为数据结构的 private: 成员始终保留。这样可以使数据安全成为包含结构的责任,减轻了结构的使用者的责任,并最大程度地减少了受影响代码的表面积。

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

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

      • 考虑进行复制以避免阻塞,当数据量较小时。这样可以让其他线程继续访问共享数据,从而可能带来更好的整体性能。

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

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

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

      • 在您指定了``-Wthread-safety``的地方,添加额外的标志``-Wthread-safety-negative``

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

  • 如何运行分析

    • ROS CI构建系统每晚会运行一个带有``libcxx``的作业,在Thread Safety Analysis引发警告时,ROS 2核心堆栈中的任何问题都将被标记为"不稳定"

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

      • 使用 colcon clang-libcxx mixin <https://github.com/colcon/colcon-mixin-repository/blob/master/clang-libcxx.mixin>`__(参见`文档 以配置 mixin):

        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
        

结果上下文:

  • 使用Clang和`libcxx`时,可能会在编译时检测到潜在的死锁和竞态条件。

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

上下文:

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

  • 您使用 pthreads 或 C++11 线程 + llvm libc++(在 ThreadSanitizer 的情况下)。

  • 您不使用 Libc/libstdc++ 的静态链接(在 ThreadSanitizer 的情况下)。

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

问题:

  • 数据竞争和死锁可能导致关键错误。

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

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

解决方案:

  • 使用一个专注于查找数据竞争和死锁的动态分析工具(例如 clang ThreadSanitizer)。

实施方法:

结果上下文:

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

  • 分析结果可能缺乏可靠性,工具处于测试阶段(在ThreadSanitizer的情况下)。

  • 生产代码仪器化导致的开销(为仪器化和非仪器化生产代码维护不同的分支等)。

  • 仪器化代码每个线程需要更多内存(在ThreadSanitizer的情况下)。

  • 仪器化代码映射了大量的虚拟地址空间(在ThreadSanitizer的情况下)。