接觸了快一年的ROS, 這段時間個人而言, 似乎完全沒在日常的程式設計中對線程/IPC有所考慮.
這說明ROS是一個很易用的架構, 在程式設計上了屏蔽了很多系統知識, 可以更加專注于邏輯/算法等.
但是從程式員的角度, 其實還是蠻不安的, 需要了解其背後的機制.
1. 背景
ROS就不再介紹了, 網上有非常多的文檔, 我們主要關注下ROS内部的一些行為.
下圖是我之前畫的ROS内部線程視角的時序圖, 看看能不能幫助了解:

一個ROS節點起來後, 主要是有5個線程:
- main
- main函數運作的線程
- pollmanager
- io運作的線程, 主要是收發topic, 然後把資料queue後待callback消費
- xmlrpcmanager
- 處理service連接配接的線程
- internalcallbackqueue
- 調用内部callback的線程
- rosoutappender
- rosout線程
其他還有asyncspinner/multithreadspinner線程可以建立, 其執行邏輯可以了解為:
thread_func() {
if (特殊callbackqueue)
call 特殊callbackqueue
else
call 全局callbackqueue
}
從topic收發的解讀了解:
- pollmanager部分:
- socket poll + recv
- 把收到的資料放到内部的data queue, 如果data queue滿則丢棄
- main或者asyncspinner部分:
- ros::spin()
- call global_callbackqueue
- 從data queue取出資料, 調用topic的callback
- ros::spin()
2. Tips
了解完背景, 下面就可以分析一些常見的問題
2.1. spinOnce使用
while ( ros::ok()) {
ros::spinOnce();
Proc();
sleep(50ms);
}
如上寫法是很常見的ros程式寫法.
ros::spinOnce()
會調用topic allback, 處理完50ms内queue裡儲存的topic資料.
這種寫法會出現幾個現象:
-
50ms調用一次, topic消費慢, 丢包嚴重spinOnce
-
耗時大/波動, 導緻Proc的延遲大/波動spinOnce
合适的寫法如下:
auto timer_func = [](void) {
Proc();
}
create_timer(timer_func);
ros::spin();
在
timer_func
外的空閑時間, 程式會處理
topic callback
, 進而減少丢包和Proc的延遲波動
2.2. AsyncSpinner/MultiThreadedSpinner使用
void callback1() {
data = 1;
}
void callback2() {
cout << data;
}
void main() {
......
ros::AsyncSpinner spinner(4); // Use 4 threads
spinner.start();
ros::waitForShutdown();
}
就是沒有鎖保護的問題....
别笑, 搜一下, 肯定有用錯的.
2.3. callback阻塞
預設情況下, 可以認為ros上所有的callback, 包括topic/timer, 全是順序執行的. 有一個地方出現阻塞/延遲, 就會引起整個節點的卡死/延遲.
void callback_a() {
複雜邏輯;
}
void callback_b() {
複雜邏輯;
}
void callback_c() {
複雜邏輯;
}
在上面的程式中, 很容易出現一種情況, 就是程式花費了大量時間在
callback_a
上, 而程式核心可能是
callback_c
, 但因為順序執行的關系, 得不到足夠的資源.
合适的做法應該是:
void callback_a() {
儲存資料;
}
void callback_b() {
儲存資料;
}
void callback_c() {
複雜邏輯;
}
這樣犧牲掉a的一些資料, 換來c的穩定執行.
2.4. topic頻率過高
jacob@ubuntu:~$ rostopic hz /tf
subscribed to [/tf]
average rate: 957.470
min: 0.000s max: 0.010s std dev: 0.00207s window: 939
jacob@ubuntu:~$ rostopic bw /tf
subscribed to [/tf]
average: 89.24KB/s
mean: 0.10KB min: 0.09KB max: 0.10KB window: 100
如上一個topic, 從帶寬上看不高, 是以我們很容易認定這個topic"無害".
但從ros内部行為了解, 會發現這種高頻的topic會無端消耗非常多的資源.
以十收一發為例:
- 發送者:
- 1w的Socket Send Per Second
- 接受者:
- 1k的Socket Read Per Second
- 1k的Callback Call Per Second(如果queue長度夠, 沒有丢包)
對發送者, 可能光是發送開銷就占到單核10-20%.
對接受者來說, 會浪費執行時間在
pollmanager/spinner
高頻topic處理上.
2.5. 訂閱過多topic
在沒有架構梳理+基于ros程式設計的情況下, 我們很容易寫出訂閱數超過10個topic的節點.
按照之前解釋的内部邏輯, 所有topic的socket都在
pollmanager
, callback都在
global callbackqueue
處理, topic間不區分重要性, 共享處理資源.
這種情況下, 如果出現上述的高頻topic, 那有可能影響到其他關鍵topic, 導緻丢包和延遲.
合适的解法:
- 架構梳理
- Topic合并, 減少Topic數量/頻率
- 不訂閱非必要的Topic
- 使用AsyncSpinner
- 要注意有可能增加程式複雜度, 不建議使用
- 合理設定queue_size, 對不重要的topic進行丢包(建議)
- topic callback不做複雜邏輯
- ros層優化, 增加qos特性, 區分topic重要性
2.6. 大topic傳輸
一個1080p/60fps的RGB圖像topic, 其帶寬是300MB/s.
如果是基于rostopic socket來傳輸, 那麼因為序列化反序列化的存在, 帶寬還要*3.
基礎背景:
在一般的x86架構下, 單線程memcpy的速度大概在2000MB+/S.
在risc(arm)架構下, 這個值會低的多, 單線程memcpy的速度大概在5-600MB+/S.
- 架構梳理足夠強勢的情況下, 可以:
- 全鍊路的圖像/lidar使用share dmabuffer輪轉, 這樣不管是CPU/GPU, 都可以高效使用
- 或者在驅動側拷貝共享記憶體輪轉
- ros傳輸優化
2.7. 訂閱topic的queue太大
很多人喜歡随意給queue設個100.
這種情況下, 就會出現一次
SpinOnce
消費100個資料的情況!
是以不要随意給不重要的topic設定大queue.
2.7. service 長連接配接/短連接配接
ros::ServiceClient client = nh.serviceClient<my_package::Foo>("my_service_name", true);
對與頻繁調用的ros service來說, 後面的true很重要, 可以建立起長連接配接.
如果不使用長連接配接, 每次ros service調用都會消耗掉毫秒級的開銷來重建立立連接配接.
3. 其他
雖然有這個那個的問題, 但是最好不要為了解決這些問題, 用上酷炫的解法.
ros這樣做的本意就是不讓開發者天馬行空.
對于項目而言, 合适的做法應該是整理出系統性的解決方法然後統一處理.