ros::spin()
這句話的意思是循環且監聽反饋函數(callback)。循環就是指程序運行到這里,就會一直在這里循環了。監聽反饋函數的意思是,如果這個節點有callback函數,那寫一句ros::spin()在這里,就可以在有對應消息到來的時候,運行callback函數里面的內容。
就目前而言,以我愚見,我覺得寫這句話適用於寫在程序的末尾(因為寫在這句話后面的代碼不會被執行),適用於訂閱節點,且訂閱速度沒有限制的情況。
ros::spinOnce()
這句話的意思是監聽反饋函數(callback)。只能監聽反饋,不能循環。所以當你需要監聽一下的時候,就調用一下這個函數。
這個函數比較靈活,尤其是我想控制接收速度的時候。配合ros::ok()效果極佳
ROS的主循環中需要不斷調用ros::spin()或ros::spinOnce(),兩者區別在於前者調用后不會再返回,而后者在調用后還可以繼續執行之后的程序。
在使用ros::spin()的情況下,一般來說在初始化時已經設置好所有消息的回調,並且不需要其他背景程序運行。這樣一來,每次消息到達時會執行用戶的回調函數進行操作,相當於程序是消息事件驅動的;而在使用ros::spinOnce()的情況下,一般來說僅僅使用回調不足以完成任務,還需要其他輔助程序的執行:比如定時任務、數據處理、用戶界面等。
關於消息接收回調機制在ROS官網上略有說明 (callbacks and spinning)。總體來說其原理是這樣的:除了用戶的主程序以外,ROS的socket連接控制進程會在后台接收訂閱的消息,所有接收到的消息並不是立即處理,而是等到spin()或者spinOnce()執行時才集中處理。所以為了保證消息可以正常接收,需要尤其注意spinOnce()函數的使用 (對於spin()來說則不涉及太多的人為因素)。
I. 對於速度較快的消息,需要注意合理控制消息隊列及spinOnce()的時間。例如,如果消息到達的頻率是100Hz,而spinOnce()的執行頻率是10Hz,那么就要至少保證消息隊列中預留的大小大於10。
II. 如果對於用戶自己的周期性任務,最好和spinOnce()並列調用。即使該任務是周期性的對於數據進行處理,例如對接收到的IMU數據進行Kalman濾波,也不建議直接放在回調函數中:因為存在通信接收的不確定性,不能保證該回調執行在時間上的穩定性。
// 示例代碼 ros::Rate r(100); while (ros::ok()) { libusb_handle_events_timeout(...); // Handle USB events ros::spinOnce(); // Handle ROS events r.sleep(); }
III. 最后說明一下將ROS集成到其他程序架構時的情況。有些圖形處理程序會將main()包裹起來,此時就需要找到一個合理的位置調用ros::spinOnce()。比如對於OpenGL來說,其中有一個方法就是采用設置定時器定時調用的方法:
// 示例代碼 void timerCb(int value) { ros::spinOnce(); } glutTimerFunc(10, timerCb, 0); glutMainLoop(); // Never returns
消息到來並不會立即執行消息處理回調函數,而是在調用ros::spin()之后,才進行消息處理的輪轉,消息回調函數統一處理訂閱話題的消息。
roscpp不會在你的應用中明確一個線程模型:也就是說即使roscpp會在幕后使用多線程管理網絡鏈接,調度等,但它不會將自己的線程暴露在你的應用中。
roscpp允許你的回調函數被任意多線程調用,如果你願意。
最后的結果可能是你的回調函數將沒有機會被調用,最常用的方法是使用ros::spin()調用。
注意:回調函數的排隊和輪轉,不會對內部的網路通信造成影響,它們僅僅會影響到用戶的回調函數何時發生。它們會影響到訂閱者隊列。因為處理你回調函數的速度,你消息到來的速度,將會決定以前的消息會不會被丟棄。
1.單線程下的輪轉
最簡單的單線程spin的例子就是ros::spin()自己。
ros::init(argc, argv, "my_node"); //初始化節點 ros::NodeHandle nh; //創建節點句柄 ros::Subscriber sub = nh.subscribe(...); //創建消息訂閱者 ... ros::spin(); //調用spin(),統一處理消息
在這里,所有的用戶回調函數將在spin()調用之后被調用.
ros::spin()不會返回,直到節點被關閉,或者調用ros::shutdown(),或者按下ctrl+C
另一個常用的模式是周期性地調用ros::spinOnce():
ros::Rate r(10); // 10 hz while (should_continue) { //... do some work, publish some messages, etc. ... ros::spinOnce(); //輪轉一次,返回 r.sleep(); //休眠 }
ros::spinOnce()將會在被調用的那一時間點調用所有等待的回調函數.
注意:ros::spin()和ros::spinOnce()函數對單線程應用很有意義,目前不會應用於多線程.
2.多線程輪轉
上面是單線程下的消息回調函數輪轉,那多線程下是什么樣子?
roscpp庫提供了一些內嵌的支持來從多線程中調用回調函數.
1) ros::MultiThreadedSpiner
它是一個阻塞型輪轉器,類似於ros::spin().
可以使用它的構造器來設定線程的個數,如果不設置或設成0,它將為每個cpu核心使用一個線程。
ros::MultiThreadedSpinner spinner(4); // Use 4 threads
spinner.spin(); // spin() will not return until the node has been shutdown
2)ros::AsyncSpinner
API : http://docs.ros.org/api/roscpp/html/classros_1_1AsyncSpinner.html
更實用的多線程輪轉是異步輪轉器(AsyncSpiner),相對於阻塞的spin()調用,它有自己的start()和stop()調用
並且在銷毀后將自動停止。
對上述MultiThreadedSpiner等效的AsyncSpiner使用如下:
ros::AsyncSpinner spinner(4); // Use 4 threads spinner.start(); ros::waitForShutdown();
3.CallbackQueue::callAvailable() and callOne()
CallbackQueue API 回調函數隊列類:
http://docs.ros.org/api/roscpp/html/classros_1_1CallbackQueue.html
可以創建一個回調函數隊列類:
#include
...
ros::CallbackQueue my_queue;
回調函數隊列類有兩種觸發其內部回調函數的方法:callAvailable()方法和callOne()方法.
前者將獲取當前可以符合條件的回調函數,並且全部觸發它們;后者將簡單地觸發隊列中最早的那個回調函數.
這兩個方法都接受一個可選的timeout超時時間,它們將在此時間之內等待一個回調函數變得符合條件。
如果這個值是0,那么,如果隊列中沒有回調函數,該方法立即返回.
4.高級主題:使用不同的回調函數隊列
默認的是所有的消息回調函數都會被壓入全局消息回調隊列.
roscpp允許使用自定義的消息回調函數隊列並分別服務。
這可以以兩種粒度實現:
1)每個subsceribe(),advertise(),advertiseService(),等
這部分可以使用高級版的方法調用原型,使用一個選項結構體指針參數.
2)每個節點句柄
這是常見的方法,使用節點句柄的setCallbackQueue()方法:
ros::NodeHandle nh;
nh.setCallbackQueue(&my_callback_queue);
這使所有的消息訂閱者,服務,定時器等的回調函數都進入my_callback_queue,而非roscpp的默認隊列.
這意味着,ros::spin()和ros::spinOnce()將不會觸發這些回調函數。
用戶自己必須額外調用這些回調函數,可以使用的是回調函數隊列類對象的callAvailable()方法和callOne()方法
應用:
將不同的回調函數分別壓進不同的回調函數隊列有下面幾個優勢:
1)長時服務:對一個服務的回調函數安排一個單獨的隊列,然后單獨地使用一個線程來調用它,可以保證不會阻塞其它回調函數
2)計算消耗回調函數:與長時服務相似,為一個費計算時間的回調函數安排一個單獨的回調隊列處理,能夠減輕應用的負擔.