质量指南:确保代码质量
本页就如何提高 ROS 2 软件包的软件质量提供指导,与《ROS 2》的 "质量实践 "部分相比,本页侧重于更具体的领域。 开发人员指南.
以下各节将讨论 ROS 2 核心、应用和生态系统软件包以及核心客户端库(C++ 和 Python)。提出解决方案的动机是出于设计和实施方面的考虑,以提高 "可靠性"、"安全性"、"可维护性"、"确定性 "等与非功能性要求相关的质量属性。
静态代码分析是软件包构建的一部分
背景:
您已开发出 C++ 生产代码。
您已创建了一个支持构建的 ROS 2 软件包,其中包含
动情
.
问题:
库级静态代码分析不作为软件包构建程序的一部分运行。
库级静态代码分析需要手动执行。
在构建新软件包版本前忘记执行库级静态代码分析的风险。
解决方案:
使用
动情
来执行静态代码分析,作为软件包构建程序的一部分。
实施情况:
插入货包
CMakeLists.txt
锉刀
...
如果(构建测试)
查找软件包(自动 要求)
ament_lint_auto_find_test_dependencies()
... endif()
...
插入
ament_lint
在软件包中加入测试依赖项package.xml
锉刀
... <包装 格式="2";>;
...
<test_depend>ament_lint_auto</test_depend>;
<test_depend>ament_lint_common</test_depend>;
... </package>;
实例:
rclcpp
:rclcpp_lifecycle
:
结果背景:
支持的静态代码分析工具
动情
作为软件包构建的一部分运行。不支持静态代码分析工具
动情
需要单独执行。
通过代码注释进行静态线程安全分析
背景:
您正在开发/调试多线程 C++ 生产代码
您可以在 C++ 代码中通过多个线程访问数据
问题
数据竞赛和死锁会导致严重的错误。
解决方案
利用 Clang 的静态 螺纹安全分析 通过注释线程代码
实施背景:
要启用线程安全分析,必须对代码进行注释,让编译器了解代码的更多语义。这些注释是特定于 Clang 的属性,例如 __属性__(能力()))
.ROS 2 不直接使用这些属性,而是提供了预处理器宏,这些宏在使用其他编译器时会被删除。
这些宏可以在 rcpputils/thread_safety_annotations.hpp
- 螺纹安全分析文件指出
线程安全分析可与任何线程库一起使用,但要求线程应用程序接口封装在具有适当注解的类和方法中
我们决定让 ROS 2 开发人员能够使用 std::
直接开发线程基元。我们不想像上面建议的那样提供自己的封装类型。
有三个 C++ 标准库值得注意
GNU 标准库
libstdc++
- 默认情况下,可通过编译器选项-stdlib=libstdc++
LLVM 标准库
libc++
(又称libcxx
) - macOS 默认,由编译器选项明确设置-stdlib=libc++
Windows C++ 标准库 - 与本使用案例无关
libcxx
标注其 std::mutex
和 std::lock_guard
线程安全分析的实现。当使用 GNU libstdc++
因此,线程安全分析不能用于非包 std::
类型
因此,要直接使用线程安全分析 std::
类型,我们必须使用 libcxx
实施:
这里的代码迁移建议并不完整--在编写(或注释现有)线程代码时,我们鼓励你根据自己的用例合理利用尽可能多的注释。不过,这个分步步骤是一个很好的起点!
对软件包/目标进行分析
当 C++ 编译器为 Clang 时,启用
-线程安全
标志。基于 CMake 的项目示例如下如果(cmake_cxx_compiler_id 比赛 "Clang";) 添加编译选项(-线程安全) # 为你的整套服务 目标编译选项(${我的目标} 公众 -线程安全) # 针对单个库或可执行文件 endif()
法典注释
第 1 步 - 注释数据成员
在任何地方找到
std::mutex
用于保护某些成员数据添加
RCPPUTILS_TSA_GUARDED_BY(mutex_name)
标注到受互斥保护的数据上
类 Foo { 公: 空白 奖励(int 数量) { 标准::锁定<;标准::储能器>; 水闸(贮存器); 酒吧 += 数量; } 空白 获取() 缢 { 返回 酒吧; } 私人: 可变 标准::储能器 贮存器; int 酒吧 rcpputils_tsa_guarded_by(贮存器) = 0; };
步骤 2 - 修复警告
在上述例子中
Foo::get
会产生编译器警告!要解决这个问题,请在返回条形码前锁定
空白 获取() 缢 { 标准::锁定<;标准::储能器>; 水闸(贮存器); 返回 酒吧; }
步骤 3--(可选但建议)重构现有代码,使其符合私有-互斥模式
线程 C++ 代码中的一个推荐模式是始终将您的
储能器
作为私人
的成员。这使得数据安全成为包含结构的关注点,从结构用户那里卸下了责任,并最大限度地减少了受影响代码的表面积。要将锁私有化,可能需要重新考虑数据的接口。这是一个很好的练习,以下是一些需要考虑的问题
您可能希望提供专门的接口,以执行需要复杂锁定逻辑的分析,例如计算互斥保护映射结构过滤集中的成员,而不是将底层结构实际返回给消费者
在数据量较小的情况下,考虑复制以避免阻塞。这样可以让其他线程继续访问共享数据,从而提高整体性能。
步骤 4--(可选)启用负能力分析
https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#negative-capabilities
负能力分析可让您指定 "调用此函数时不得持有此锁"。它可以揭示其他注释无法揭示的潜在死锁情况。
您指定的位置
-线程安全
,添加额外的标记-线程安全-阴性
在任何获取锁的函数上,使用
RCPPUTILS_TSA_REQUIRES(!mutex)
模式
如何进行分析
ROS CI 构建农场每晚运行一项工作,其中包括
libcxx
当线程安全分析提出警告时,它将被标记为 "不稳定",从而揭示 ROS 2 核心堆栈中的任何问题对于本地运行,您可以使用以下选项,所有选项都是等价的
请使用 colcon clang-libcxx mixin 见 文献资料 用于配置混合脚本)
胶管 构建 --混杂 铛-libcxx
将编译器传递给 CMake
胶管 构建 --cmake-参数 -dcmake_c_compiler=铛 -dcmake_cxx_compiler=铛++ -dcmake_cxx_flags='-stdlib=libc++ -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS'; -dforce_build_vendor_pkg=关于 --没有-警示-闲置-挛
覆盖系统编译器
CC=铛 CXX=铛++ 胶管 构建 --cmake-参数 -dcmake_cxx_flags='-stdlib=libc++ -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS'; -dforce_build_vendor_pkg=关于 --没有-警示-闲置-挛
结果背景:
在编译时,当使用 Clang 和
libcxx
动态分析(数据竞赛 & 死锁)
背景:
您正在开发/调试您的多线程 C++ 生产代码。
您可以使用 pthreads 或 C++11 线程 + llvm libc++(如果使用 ThreadSanitizer)。
不使用 Libc/libstdc++ 静态链接(在使用 ThreadSanitizer 时)。
您不会构建与位置无关的可执行文件(在 ThreadSanitizer 的情况下)。
问题
数据竞赛和死锁会导致严重的错误。
使用静态分析无法检测到数据竞赛和死锁(原因:静态分析的局限性)。
在开发调试/测试过程中不得出现数据竞赛和死锁(原因:通常没有对生产代码中所有可能的控制路径进行测试)。
解决方案
使用侧重于查找数据竞赛和死锁的动态分析工具(此处为 clang ThreadSanitizer)。
实施:
使用选项
-fsanitize=线程
(生产代码)。如果在分析过程中要执行不同的生产代码,可考虑进行条件编译,例如 线程消毒器 _has_feature(thread_sanitizer).
如果某些代码不需要检测,则应考虑 ThreadSanitizers _/*属性*/_((no_sanitize("线程"))).
如果某些文件不应被检测,可考虑文件或函数级排除 列入黑名单的主题除尘器更具体地说: ThreadSanitizers 消毒剂特例列表 或与 ThreadSanitizers no_sanitize("线程") 并使用选项
--fsanitize-黑名单
.
结果背景:
在部署前发现生产代码中的数据竞赛和死锁的几率更高。
分析结果可能缺乏可靠性,工具还处于测试阶段(就 ThreadSanitizer 而言)。
生产代码工具化造成的开销(为工具化/非工具化生产代码维护不同分支等)。
每个线程需要更多内存(在使用 ThreadSanitizer 的情况下)。
检测代码会映射大量虚拟地址空间(以 ThreadSanitizer 为例)。