质量指南:确保代码质量 [2231]
本页面提供关于如何改善ROS 2软件包的软件质量的指导,重点是比《开发者指南》的质量实践部分更具体的领域。 [2232]
以下各节旨在解决ROS 2核心、应用和生态系统软件包以及核心客户端库(C++和Python)的问题。所提出的解决方案受到设计和实现考虑的推动,以改善"可靠性"、"安全性"、"可维护性"、"确定性"等质量属性,这些属性与非功能性需求有关。 [2233]
作为ament软件包构建的一部分的静态代码分析 [2234]
上下文: [2235]
问题: [2238]
解决方案: [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]
通过代码注释进行静态线程安全性分析 [2257]
上下文: [2258]
问题: [2261]
数据竞争和死锁可能导致关键错误。 [2262]
解决方案: [2263]
通过为线程代码添加注释,利用Clang的静态`Thread Safety Analysis <https://clang.llvm.org/docs/ThreadSafetyAnalysis.html>`__ [2264]
实现的上下文: [2265]
要启用线程安全分析,必须对代码进行注释,以便编译器了解代码的语义。这些注释是特定于Clang的属性,例如``__attribute__(capability()))``。ROS 2提供了预处理宏来代替直接使用这些属性,在使用其他编译器时这些宏会被擦除。 [2266]
这些宏可以在 rcpputils/thread_safety_annotations.hpp 中找到 [2267]
我们决定允许 ROS 2 开发人员直接使用 std::
线程原语进行开发,而不提供上述建议中的自定义包装类型 [2270]
有三个 C++ 标准库需要注意 [2271]
GNU 标准库
libstdc++
- 在 Linux 上为默认选项,可以通过编译器选项-stdlib=libstdc++
显式设置 [2272]LLVM 标准库
libc++``(也称为 ``libcxx
)- 在 macOS 上为默认选项,可以通过编译器选项-stdlib=libc++
显式设置 [2273]Windows C++ 标准库 - 对于此用例无关 [2274]
libcxx
在其 std::mutex
和 std::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]
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]
步骤4 - (可选)启用负能力分析 [2292]
https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#negative-capabilities [2293]
负能力分析允许您指定“调用此函数时不得持有此锁”。它可以揭示其他注释无法发现的潜在死锁情况。 [2294]
如何运行分析 [2297]
ROS CI构建系统每晚会运行一个带有``libcxx``的作业,在Thread Safety Analysis引发警告时,ROS 2核心堆栈中的任何问题都将被标记为"不稳定" [2298]
对于本地运行,您有以下选项,全部等效 [2299]
使用 colcon clang-libcxx mixin <https://github.com/colcon/colcon-mixin-repository/blob/master/clang-libcxx.mixin>`__(参见`文档 以配置 mixin):: [2300]
colcon build --mixin clang-libcxx
将编译器传递给 CMake:: [2301]
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
覆盖系统编译器:: [2302]
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
结果上下文: [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]
使用选项“-fsanitize=thread”将生产代码编译和链接到 clang 中(这会对生产代码进行插桩)。 [2313]
如果在分析期间需要执行不同的生产代码,请考虑条件编译,例如 ThreadSanitizers _has_feature(thread_sanitizer)。 [2314]
如果有些代码不需要进行插桩,考虑使用 ThreadSanitizers _/*attribute*/_((no_sanitize("thread")))。 [2315]
如果有些文件或函数不需要进行插桩,考虑使用文件级别或函数级别的排除 ThreadSanitizers黑名单,更具体地说,使用 ThreadSanitizers Sanitizer特例列表 或者使用 ThreadSanitizers no_sanitize("thread") 并且使用选项
--fsanitize-blacklist
。 [2316]
结果上下文: [2317]