质量指南:确保代码质量
本页面提供关于如何改善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>
示例:
rclcpp
:rclcpp_lifecycle
:
产生的上下文:
通过代码注释进行静态线程安全性分析
上下文:
您正在开发/调试多线程的C++生产代码
您在C++代码中从多个线程访问数据
问题:
数据竞争和死锁可能导致关键错误。
解决方案:
通过为线程代码添加注释,利用Clang的静态`Thread Safety Analysis <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++代码中,推荐的模式是将
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)。
实施方法:
使用选项“-fsanitize=thread”将生产代码编译和链接到 clang 中(这会对生产代码进行插桩)。
如果在分析期间需要执行不同的生产代码,请考虑条件编译,例如 ThreadSanitizers _has_feature(thread_sanitizer)。
如果有些代码不需要进行插桩,考虑使用 ThreadSanitizers _/*attribute*/_((no_sanitize("thread")))。
如果有些文件或函数不需要进行插桩,考虑使用文件级别或函数级别的排除 ThreadSanitizers黑名单,更具体地说,使用 ThreadSanitizers Sanitizer特例列表 或者使用 ThreadSanitizers no_sanitize("thread") 并且使用选项
--fsanitize-blacklist
。
结果上下文:
在部署之前,生产代码中更有可能发现数据竞争和死锁问题。
分析结果可能缺乏可靠性,工具处于测试阶段(在ThreadSanitizer的情况下)。
生产代码仪器化导致的开销(为仪器化和非仪器化生产代码维护不同的分支等)。
仪器化代码每个线程需要更多内存(在ThreadSanitizer的情况下)。
仪器化代码映射了大量的虚拟地址空间(在ThreadSanitizer的情况下)。