质量指南:确保代码质量 [2231]

本页面提供关于如何改善ROS 2软件包的软件质量的指导,重点是比《开发者指南》的质量实践部分更具体的领域。 [2232]

以下各节旨在解决ROS 2核心、应用和生态系统软件包以及核心客户端库(C++和Python)的问题。所提出的解决方案受到设计和实现考虑的推动,以改善"可靠性"、"安全性"、"可维护性"、"确定性"等质量属性,这些属性与非功能性需求有关。 [2233]

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

上下文[2235]

  • 您已经开发了您的C++生产代码。 [2236]

  • 您已经创建了一个具有``ament``构建支持的ROS 2包。 [2237]

问题[2238]

  • 在软件包构建过程中,不会运行库级静态代码分析。 [2239]

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

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

解决方案[2242]

  • 使用 ament 的集成能力,在软件包构建过程中执行静态代码分析。 [2243]

实现[2244]

  • 将其插入到软件包的 CMakeLists.txt 文件中。 [2245]

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

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

示例: [2247]

产生的上下文[2254]

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

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

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

上下文: [2258]

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

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

问题: [2261]

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

解决方案: [2263]

实现的上下文: [2265]

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

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

线程安全分析文档说明如下 [2268]

线程安全分析可以与任何线程库一起使用,但需要将线程 API 包装在具有适当注释的类和方法中 [2269]

我们决定允许 ROS 2 开发人员直接使用 std:: 线程原语进行开发,而不提供上述建议中的自定义包装类型 [2270]

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

  • GNU 标准库 libstdc++ - 在 Linux 上为默认选项,可以通过编译器选项 -stdlib=libstdc++ 显式设置 [2272]

  • LLVM 标准库 libc++``(也称为 ``libcxx)- 在 macOS 上为默认选项,可以通过编译器选项 -stdlib=libc++ 显式设置 [2273]

  • Windows C++ 标准库 - 对于此用例无关 [2274]

libcxx 在其 std::mutexstd::lock_guard 实现中添加了线程安全分析的注释。当使用 GNU libstdc++ 时,这些注释不存在,因此无法在未包装的 std:: 类型上使用线程安全分析。 [2275]

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

实施方法: [2277]

这里提供的代码迁移建议并不完整 - 在编写(或注释现有)线程代码时,建议根据您的使用情况尽可能多地使用注释。然而,这个逐步过程是一个很好的起点! [2278]

  • 启用包/目标的分析功能 [2279]

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

    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()
    
  • 注释代码 [2281]

    • 步骤1 - 注释数据成员 [2282]

      • 找到任何使用 std::mutex 保护某些成员数据的地方 [2283]

      • 在由互斥锁保护的数据上添加 RCPPUTILS_TSA_GUARDED_BY(mutex_name) 注释 [2284]

      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 - 修复警告 [2285]

      • 在上面的示例中,Foo::get 会产生编译器警告!为了修复它,在返回 bar 之前进行锁定 [2286]

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

      在使用多线程的C++代码中,推荐的模式是将 mutex 作为数据结构的 private: 成员始终保留。这样可以使数据安全成为包含结构的责任,减轻了结构的使用者的责任,并最大程度地减少了受影响代码的表面积。 [2288]

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

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

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

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

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

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

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

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

  • 如何运行分析 [2297]

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

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

结果上下文: [2303]

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

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

上下文: [2258]

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

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

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

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

问题: [2261]

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

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

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

解决方案: [2263]

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

实施方法: [2277]

结果上下文: [2317]

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

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

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

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

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