天天看點

ROS程式設計: 一些Tips1. 背景2. Tips3. 其他

接觸了快一年的ROS, 這段時間個人而言, 似乎完全沒在日常的程式設計中對線程/IPC有所考慮.

這說明ROS是一個很易用的架構, 在程式設計上了屏蔽了很多系統知識, 可以更加專注于邏輯/算法等.

但是從程式員的角度, 其實還是蠻不安的, 需要了解其背後的機制.

1. 背景

ROS就不再介紹了, 網上有非常多的文檔, 我們主要關注下ROS内部的一些行為.

下圖是我之前畫的ROS内部線程視角的時序圖, 看看能不能幫助了解:

ROS程式設計: 一些Tips1. 背景2. Tips3. 其他

一個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

2. Tips

了解完背景, 下面就可以分析一些常見的問題

2.1. spinOnce使用

while ( ros::ok()) {
    ros::spinOnce();
    Proc();
    sleep(50ms);
}
           

如上寫法是很常見的ros程式寫法.

ros::spinOnce()

會調用topic allback, 處理完50ms内queue裡儲存的topic資料.

這種寫法會出現幾個現象:

  • spinOnce

    50ms調用一次, topic消費慢, 丢包嚴重
  • spinOnce

    耗時大/波動, 導緻Proc的延遲大/波動

合适的寫法如下:

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這樣做的本意就是不讓開發者天馬行空.

對于項目而言, 合适的做法應該是整理出系統性的解決方法然後統一處理.

繼續閱讀