调用NodeHandle::subscribe()可得到Subscriber对象,同时会在Ros系统创建一个订阅连接,这里将描述NodeHandle、Subscriber、Subscriber::Impl如何管理这个连接。对Subscriber的相关结论可以类推到Publisher、ServiceServer、ServiceClient。
- 可认为Subscriber由两部分组成,一是订阅连接,二是壳。变量Subscriber::Impl_表示订阅连接,类型shared_ptr<Subscriber::Impl>,其它的合称为壳。
- Subscriber::Impl_不是nullptr时,并不表示连接就有效,它可能已经被销毁(unsubscribe)。判断是否有效须使用Impl::isValid()。如果只拥有一个Subscriber对象指针,目前没办法通过该指针判断该Subscriber是否持有订阅连接。
- Ros以shared_ptr<Subscriber::Impl>管理订阅连接。NodeHandle::subscribe()时引用计数赋初值1,该Subscriber赋值给另一个Subscriber时,引用计数加1。~Subscriber让引用计数减1,减到0时,会销毁该连接。
- 销毁订阅连接指的是该连接从Ros中消失,消失不是要求析构掉之前持有过该连接的所有Subscriber,如何不析构Subscriber而销毁连接,使用Subscriber::shutdown。
- 要保证订阅连接(Subscriber::Impl)一直有效,至少得一直存在一个持有该连接的Subscriber。甚至可以没有创建该Subscriber的NodeHandle。
- Subscriber::shutdown()的作用是强制销毁(unsubscribe)内中连接,它不管Subscriber::Impl_当前有多少引用计数。一旦销毁,对之前持有过该连接的Subscriber,它们中Impl不会变成nullptr,是Impl一些成员被置为nullptr。如果调用shutdown时,Subscriber已没有有效连接,那它啥也不干。
- NodeHandle拥有Subscriber::Impl_的weak_ptr,放在数组变量NodeHandle::collection_->subs_。持有这个指针目的:在执行NodeHandle::shutdown()时,可以强制销毁掉subs_中依旧有效的连接(通过直接调用Impl::unsubscribe)。但是,若没特殊要要,不会调用NodeHandle::shutdown,于是可认为NodeHandle和存储订阅连接无关。注:NodeHandle::shutdown()不是减少Subscriber::Impl_的引用计数,而是销毁。
- 销毁订阅连接后,NodeHandle::collection_->subs_依旧保存着对它的weak_ptr。
- NodeHandle::~NodeHandle不会调用NodeHandle::shutdown(),或许,就不该存在NodeHandle::shutdown。
- TopicManager::subscriptions_存储着该进程subscribe过的订阅连接(关于TopicManager::subscriptions_参考“Publisher::publish源码分析”)。通过(Impl_.topic_+Impl_.helper_),Subscriber::Impl_从subscriptions_定位出自已。执行Impl::unsubscribe时,会把helper_置nullptr,unsubscribed_置true,但不会修改topic_。也就是说,不要根据Subscriber::Impl_.topic_是不是空来判断该订阅连接是否有效,须使用Impl::isValid()。
- 有两种方法可销毁订阅连接。一是用一个持有该连接的Subscriber,调用成员方法shutdown。二是析构掉持有该连接的所有Subscriber,使得Subscriber::Impl引用计数是0,导致调用~Impl,~Impl会执行销毁(unsubscribe)。
- 订阅连接一旦被销毁,TopicManager::subscriptions_中就没它的信息了,CallbackQueue收到msg时,自然不会再回调该连接中的回调操作。
- 不必担心先析构NodeHandle、后析构Subscriber会导致问题。由于Subscriber::Impl内持有一个“新”NodeHandle,“新”字加引号,因为它是由传入的NodeHandle复制创建。析构NodeHandle后,不会影响Impl内的NodeHandle,有这个NodeHandle,后面析构Subscriber时不会出问题。
- 系统中最后一个NodeHandle被析构时,~NodeHandle会调用ros::shutdown()。ros::shutdown和订阅连接没任何关系,在调用它前,系统应该已没订阅连接。不要主动调用ros::shutdown(),应该由Ros判断出这是最后一次析构NodeHandle时自动调用。假设系统只存在两个变量,一个NodeHandle,一个Subscriber,会出现析构NodeHandle时不调用ros::shutdown,析构Subscriber时反而调用。因为析构Subscriber导致析构Impl,析构Impl导致析构内中的NodeHandle,这内中的NodeHandle才是最后一个。
一、NodeHandle::subscribe(SubscribeOptions& ops)
<libros>/ros_comm/roscpp/src/libros/node_handle.cpp ------ Subscriber NodeHandle::subscribe(SubscribeOptions& ops) { ...... if (TopicManager::instance()->subscribe(ops)) { Subscriber sub(ops.topic, *this, ops.helper); { boost::mutex::scoped_lock lock(collection_->mutex_); collection_->subs_.push_back(sub.impl_); } return sub; } return Subscriber(); } bool TopicManager::subscribe(const SubscribeOptions& ops)
NodeHandle::subscribe填充好ops后,调用TopicManager::instance()->subscribe,输入参数ops冠上const修饰符,即TopicManager::subscribe不会修改ops,NodeHandle::subscribe是根据判断返回值知道subscribe是否成功订阅。一旦成功,会根据此次连接的关键信息“topic+helper”生成Impl。注意生成Impl的第二个参数NodeHandle,因为Impl在存储它时会创建一个新的,而不是引用计数加一。所以在析构Impl时,是销毁这个新NodeHandle,而不是构造时传入的nh。
sub.impl_存储着此次订阅连接,impl_类型是shared_ptr<Impl>,但经过push_back后,存在subs_中的将变成weak_ptr<Impl>。
1.1 NodeHandle.collection_.subs_
<libros>/include/ros/node_handle.h ------ class ROSCPP_DECL NodeHandle { NodeHandleBackingCollection* collection_; }; <libros>/ros_comm/roscpp/src/libros/node_handle.cpp ------ class NodeHandleBackingCollection { public: typedef std::vector<Publisher::ImplWPtr> V_PubImpl; typedef std::vector<ServiceServer::ImplWPtr> V_SrvImpl; typedef std::vector<Subscriber::ImplWPtr> V_SubImpl; typedef std::vector<ServiceClient::ImplWPtr> V_SrvCImpl; V_PubImpl pubs_; V_SrvImpl srvs_; V_SubImpl subs_; V_SrvCImpl srv_cs_; boost::mutex mutex_; // keep shared_ptrs to these managers to avoid assertions. Fixes #838 TopicManagerPtr keep_alive_topic_manager = TopicManager::instance(); ServiceManagerPtr keep_alive_service_manager = ServiceManager::instance(); };
在NodeHandle内部,Subscriber::Impl_放在成员变量collection_->subs_,类型变成了Subscriber::ImplWPtr,让看ImplWPtr定义。
<libros>/include/ros/subscriber.h ------ class ROSCPP_DECL Subscriber { class Impl { public: Impl(); ~Impl(); void unsubscribe(); bool isValid() const; std::string topic_; NodeHandlePtr node_handle_; SubscriptionCallbackHelperPtr helper_; bool unsubscribed_; }; typedef boost::shared_ptr<Impl> ImplPtr; typedef boost::weak_ptr<Impl> ImplWPtr; ImplPtr impl_; };
Subscriber::ImplWPtr是指向Subscriber::Impl的boost::weak_ptr。也就是说,它不会增加、减少Subscriber::ImplPtr引用计数,只是像旁观者那样观察Subscriber::ImplPtr使用情况,必要时可通过lock方法得到ImplPtr。
由于NodeHandle拥有Subscriber::ImplWPtr,在需要时,它会通过lock判断该Subscriber::ImplWPtr是否还有效,有可能会执行操作,像销毁(unsubscribe),见底下的NodeHandle::shutdown()。
subs_变量类型是std::vector<Subscriber::ImplWPtr>,不能依此就得出结论,说此个NodeHandle拥有多少个Subscriber::Impl,此个std::vector就是多少长度,结论应该“Impl数 <= vector.size”。一旦出现有Subscriber::Impl已经被unsubscriber,系统中就不存在该连接了,但不会从vector中erase。须通过lock方法判断vector中的Impl是否还有效,正如shutdown。
<libros>/ros_comm/roscpp/src/libros/node_handle.cpp ------ void NodeHandle::shutdown() { { NodeHandleBackingCollection::V_SubImpl::iterator it = collection_->subs_.begin(); NodeHandleBackingCollection::V_SubImpl::iterator end = collection_->subs_.end(); for (; it != end; ++it) { Subscriber::ImplPtr impl = it->lock(); if (impl) { impl->unsubscribe(); } } } ...... }
综上所述,NodeHandle拥有Subscriber::Impl_的weak_ptr,放在数组变量NodeHandle::collection_->subs_。持有这个指针目的:在执行NodeHandle::shutdown()时,可以强制销毁掉subs_中依旧有效的连接(通过直接调用Impl::unsubscribe)。但是,若没特殊要要,不会调用NodeHandle::shutdown,于是可认为NodeHandle和存储订阅连接无关。
二、销毁订阅连接:Subscriber析构和Subscriber::shutdown
Impl::unsubscribe执行销毁订阅连接,Subscriber析构和Subscriber::shutdown都会调用到它,都可达到销毁连接这个目的,但途径不一样。
<libros>/ros_comm/roscpp/src/libros/subscriber.cpp ------ void Subscriber::Impl::unsubscribe() { if (!unsubscribed_) { unsubscribed_ = true; TopicManager::instance()->unsubscribe(topic_, helper_); node_handle_.reset(); helper_.reset(); } } Subscriber::Impl::~Impl() { unsubscribe(); } void Subscriber::shutdown() { if (impl_) { impl_->unsubscribe(); } }
- 销毁之Subscriber析构。Impl_是Subscriber成员变量,析构Subscriber时,会使Impl_引用计数减1,当减少到0时会调用~Impl。因为~Impl会调用unsubscribe,于是导致了销毁。
- 销毁之Subscriber::shutdown。它不管impl_当前引用计数是多少,强制销毁。
Impl::unsubscribe()销毁订阅连接时,要调用TopicManager::instance()->unsubscribe,后者通过此次连接的关键信息“topic+helper”,在TopicManager::subscriptions_定位出自已所在的Subscription,并从Subscription::callbacks_删除自个信息,如果删除后callbacks_空了,意味着进程内已没订阅该topic的Subscriber,同时删除Subscription。