质量指南:确保代码质量
本页提供有关如何提高 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:
rclcpp
:rclcpp_lifecycle
:
Resulting context:
ament
支持的静态代码分析工具作为软件包构建的一部分运行。ament
不支持的静态代码分析工具需要单独执行。
通过代码注释进行静态线程安全分析
上下文:
您正在开发/调试多线程 C++ 生产代码
您在 C++ 代码中从多个线程访问数据
问题:
数据争用和死锁可能导致严重错误。
Solution:
通过注释线程代码来利用 Clang 的静态“线程安全分析<https://clang.llvm.org/docs/ThreadSafetyAnalysis.html>”
实施上下文:
要启用线程安全分析,必须对代码进行注释,以便让编译器更多地了解代码的语义。 这些注释是 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::mutex
和 std::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)。
实施:
使用选项
-fsanitize=thread
用 clang 编译并链接生产代码(这会对生产代码进行检测)。如果在分析期间需要执行不同的生产代码,请考虑条件编译,例如 ThreadSanitizers _has_feature(thread_sanitizer)。
如果某些代码不需要检测,请考虑 ThreadSanitizers _/*attribute*/_((no_sanitize(“thread”)))。
如果某些文件不应被检测,请考虑文件或函数级排除`ThreadSanitizers 黑名单 <https://clang.llvm.org/docs/ThreadSanitizer.html#ignorelist>`__,更具体地说:ThreadSanitizers Sanitizer Special Case List 或使用 ThreadSanitizers no_sanitize(“thread”) 并使用选项
--fsanitize-blacklist
。
结果上下文:
在部署生产代码之前,更有可能在其中发现数据竞争和死锁。
分析结果可能缺乏可靠性,工具处于测试阶段(就 ThreadSanitizer 而言)。
由于生产代码检测而产生的开销(为已检测/未检测的生产代码维护单独的分支等)。
已检测的代码需要每个线程更多的内存(对于 ThreadSanitizer)。
已检测的代码映射大量虚拟地址空间(对于 ThreadSanitizer)。