一、背景
在為什么需要異步編程文章末尾提到,"為了使socket和緩沖區(read或write)在整個異步操作的生命周期一直保持活動,我們需要采取特殊的保護措施。你的連接類需要繼承自enabled_shared_from_this,然后在內部保存它需要的緩沖區,而且每次異步調用都要傳遞一個智能指針給this操作"。本文就詳細介紹為什么使用enabled_shared_from_this就能保證對象的生命周期,以及enabled_shared_from_this內部的具體實現分析。
二、為什么需要保證對象生命周期
首先想象下同步編程,比如socket建立connect后,read或者write數據,因為是同步阻塞的,數據傳輸完后,socket對象就已經完成了此次任務,此時就算對象銷毀,也並不會引起異常。但是異步編程就不一樣了,當一個線程調用一個異步函數(例如:該函數還是socket寫文件任務),該函數會立即返回,盡管規定的任務還沒有完成,這樣線程就會執行異步函數的下一條語句,而不會被掛起。只有當"寫文件任務"完成后,由新的線程發送完成消息來執行結果同步,但是當新的線程完成"寫文件任務"后,再發送過來,此時異步函數調用方對象是否還存在,這就是個需要解決的問題,這也就是為什么需要保證對象的生命周期。
更加直白一點的例子,假設你需要做下面的操作:
io_service service;
ip::tcp::socket sock(service);
char buff[512];
...
read(sock, buffer(buff));
在這個例子中,sock和buff的存在時間都必須比read()調用的時間要長。也就是說,在調用read()返回之前,它們都必須有效。你傳給一個方法的所有參數在方法內部都必須有效。當我們采用異步方式時,事情會變得比較復雜。
io_service service;
ip::tcp::socket sock(service);
char buff[512];
void on_read(const boost::system::error_code &, size_t) {}
...
async_read(sock, buffer(buff), on_read);
在這個例子中,sock和buff的存在時間都必須比async_read()操作本身時間要長,但是read操作持續的時間我們是不知道的,因為它是異步的。當socket滿足條件,有數據可讀時,此時操作系統會把數據發送到緩沖區,觸發async_read的回調函數on_read執行,on_read執行來通過socket讀取數據到buffer,所以必須socket和buffer的生命周期要能得到保證。那究竟用什么方法呢?
三、實踐中使用方法
異步編程時,我們在傳入回調函數的時候,通常會想要其帶上當前類對象的上下文,或者回調本身就是類成員函數,那這個工作自然非this指針莫屬了,像這樣:
void sock_sender::post_request_no_lock()
{
Request &req = requests_.front();
boost::asio::async_write(
*sock_ptr_,
boost::asio::buffer(req.buf_ptr->get_content()),
boost::bind(&sock_sender::self_handler, this, _1, _2));
}
然而回調執行的時候並一定對象還存在。為了確保對象的生命周期大於回調,我們可以使類繼承自enable_shared_from_this,然后回調的時候使用bind傳入shared_from_this()返回的智能指針。由於bind保存的是參數的副本,bind構造的函數對象會一直持有一個當前類對象的智能指針而使其引用計數不為0,這就確保了對象的生命周期大於回調中構造的函數對象的生命周期,像這樣:
class sock_sender : public boost::enable_shared_from_this<sock_sender>
{
//...
};
void sock_sender::post_request_no_lock()
{
Request &req = requests_.front();
boost::asio::async_write(
*sock_ptr_,
boost::asio::buffer(req.buf_ptr->get_content()),
boost::bind(&sock_sender::self_handler, shared_from_this(), _1, _2));
}
“實際上邊已經提到了,延長資源的生命周期防止使用它時已經被釋放。這種問題絕大部分出現在異步調用的時候。因為異步函數的執行時間點無法確定。異步函數可能會使用異步調用之前的變量(比如類對象),這樣就必須保證該變量在異步執行期間有效。如何做到這一點呢?只需要傳遞一個指向自身的shared_ptr(必須使用shared_from_this())給異步函數。因為這個拷貝過程使得對資源的引用計數加一。
四、關於enable_shared_from_this的原理分析
首先要說明的一個問題是:如何安全地將this指針返回給調用者。一般來說,我們不能直接將this指針返回。
想象這樣的情況,該函數將this指針返回到外部某個變量保存,然后這個對象自身已經析構了,但外部變量並不知道,此時如果外部變量使用這個指針,就會使得程序崩潰。
使用智能指針shared_ptr看起來是個不錯的解決方法。但問題是如何去使用它呢?我們來看如下代碼:
#include <iostream>
#include <boost/shared_ptr.hpp>
class Test
{
public:
//析構函數
~Test() { std::cout << "Test Destructor." << std::endl; }
//獲取指向當前對象的指針
boost::shared_ptr<Test> GetObject()
{
boost::shared_ptr<Test> pTest(this);
return pTest;
}
};
int main(int argc, char *argv[])
{
{
boost::shared_ptr<Test> p( new Test( ));
std::cout << "q.use_count(): " << q.use_count() << std::endl;
boost::shared_ptr<Test> q = p->GetObject();
}
return 0;
}
運行后,程序輸出:
Test Destructor.
q.use_count(): 1
Test Destructor.
可以看到,對象只構造了一次,但卻析構了兩次。並且在增加一個指向的時候,shared_ptr的計數並沒有增加。也就是說,這個時候,p和q都認為自己是Test指針的唯一擁有者,這兩個shared_ptr在計數為0的時候,都會調用一次Test對象的析構函數,所以會出問題。
那么為什么會這樣呢?給一個shared_ptr
答案是:對的,shared_ptr
看這樣的代碼:
int main()
{
Test* test = new Test();
shared_ptr<Test> p(test);
shared_ptr<Test> q(test);
std::cout << "p.use_count(): " << p.use_count() << std::endl;
std::cout << "q.use_count(): " << q.use_count() << std::endl;
return 0;
}
運行后,程序輸出:
p.use_count(): 1
q.use_count(): 1
Test Destructor.
Test Destructor.
也證明了剛剛的論述:shared_ptr
事實上,類對象是由外部函數通過某種機制分配的,而且一經分配立即交給 shared_ptr管理,而且以后凡是需要共享使用類對象的地方,必須使用這個 shared_ptr當作右值來構造產生或者拷貝產生(shared_ptr類中定義了賦值運算符函數和拷貝構造函數)另一個shared_ptr ,從而達到共享使用的目的。
解釋了上述現象后,現在的問題就變為了:如何在類對象(Test)內部中獲得一個指向當前對象的shared_ptr 對象?(之前證明,在類的內部直接返回this指針,或者返回return shared_ptr
如果我們能夠做到這一點,直接將這個shared_ptr對象返回,就不會造成新建的shared_ptr的問題了。
下面來看看enable_shared_from_this類的威力。
enable_shared_from_this 是一個以其派生類為模板類型參數的基類模板,繼承它,派生類的this指針就能變成一個 shared_ptr。
有如下代碼:
#include <iostream>
#include <memory>
class Test : public std::enable_shared_from_this<Test> //改進1
{
public:
//析構函數
~Test() { std::cout << "Test Destructor." << std::endl; }
//獲取指向當前對象的指針
std::shared_ptr<Test> GetObject()
{
return shared_from_this(); //改進2
}
};
int main(int argc, char *argv[])
{
{
std::shared_ptr<Test> p( new Test( ));
std::shared_ptr<Test> q = p->GetObject();
std::cout << "p.use_count(): " << p.use_count() << std::endl;
std::cout << "q.use_count(): " << q.use_count() << std::endl;
}
return 0;
}
運行后,程序輸出:
p.use_count(): 2
q.use_count(): 2
Test Destructor.
可以看到,問題解決了!只有一次new對象,那么釋放的時候也就一次,不會出現兩次而引起程序崩潰。但是要說明的是,這里舉的例子是兩個shared_ptr
struct connection : boost::enable_shared_from_this<connection> {
typedef boost::shared_ptr<connection> ptr;
void start(ip::tcp::endpoint ep) {
sock_.async_connect(ep, boost::bind(&connection::on_connect, shared_from_this(), _1));
}
};
int main(int argc, char* argv[]) {
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 8001);
connection::ptr(new connection)->start(ep);
}
1、這里的connection::ptr(new connection)->start(ep);能否用普通new的指針,而沒有被shared_ptr托管的指針? 答案是不能,原因見后面說明2。
2、這段server端的代碼,每當有不同client連過來,就會觸發on_connect回調函數執行。在所有異步調用中,我們傳遞一個boost::bind仿函數當作參數。這個仿函數內部包含了一個智能指針,指向connection實例。只要有一個異步操作等待時,Boost.Asio就會保存boost::bind仿函數的拷貝,這個拷貝保存了指向連接實例的一個智能指針,從而保證connection實例保持活動。問題解決!
接着來看看enable_shared_from_this 是如何工作的,以下是它的源碼:
template<class T> class enable_shared_from_this
{
protected:
BOOST_CONSTEXPR enable_shared_from_this() BOOST_SP_NOEXCEPT
{ }
BOOST_CONSTEXPR enable_shared_from_this(enable_shared_from_this const &) BOOST_SP_NOEXCEPT
{ }
enable_shared_from_this & operator=(enable_shared_from_this const &) BOOST_SP_NOEXCEPT
{
return *this;
}
~enable_shared_from_this() BOOST_SP_NOEXCEPT // ~weak_ptr<T> newer throws, so this call also must not throw
{
}
public:
shared_ptr<T> shared_from_this()
{
shared_ptr<T> p( weak_this_ );
BOOST_ASSERT( p.get() == this );
return p;
}
shared_ptr<T const> shared_from_this() const
{
shared_ptr<T const> p( weak_this_ );
BOOST_ASSERT( p.get() == this );
return p;
}
weak_ptr<T> weak_from_this() BOOST_SP_NOEXCEPT
{
return weak_this_;
}
weak_ptr<T const> weak_from_this() const BOOST_SP_NOEXCEPT
{
return weak_this_;
}
public: // actually private, but avoids compiler template friendship issues
// Note: invoked automatically by shared_ptr; do not call
template<class X, class Y> void _internal_accept_owner( shared_ptr<X> const * ppx, Y * py ) const BOOST_SP_NOEXCEPT
{
if( weak_this_.expired() )
{
weak_this_ = shared_ptr<T>( *ppx, py );
}
}
private:
mutable weak_ptr<T> weak_this_;
};
} // namespace boost
#endif // #ifndef BOOST_SMART_PTR_ENABLE_SHARED_FROM_THIS_HPP_INCLUDED
其中shared_from_this()函數的實現為:
shared_ptr<T> shared_from_this()
{
shared_ptr<T> p( weak_this_ );
BOOST_ASSERT( p.get() == this );
return p;
}
可以看見,這個函數使用了weak_ptr對象(weak_this)來構造一個shared_ptr對象,然后將shared_ptr對象返回。注意這個weak_ptr是實例對象的一個成員變量,所以對於一個對象來說,它一直是同一個,每次調用shared_from_this()時,就會根據weak_ptr來構造一個臨時shared_ptr對象。
也許看到這里會產生疑問,這里的shared_ptr也是一個臨時對象,和前面有什么區別?還有,為什么enable_shared_from_this 不直接保存一個 shared_ptr 成員?
對於第一個問題,這里的每一個shared_ptr都是根據weak_ptr來構造的,而每次構造shared_ptr的時候,使用的參數是一樣的,所以這里根據相同的weak_ptr來構造多個臨時shared_ptr等價於用一個shared_ptr來做拷貝。(在以后調用 shared_from_this() 函數時, 就能利用 weak_this 調用 shared_ptr 的構造函數, 從而生成一個共享同一對象的 shared_ptr,其實簡單點理解的話就是:shared_from_this強迫只能是shared_ptr對象才能調用,這樣從源頭保證共享同一對象)(PS:在shared_ptr類中,是有使用weak_ptr對象來構造shared_ptr對象的構造函數的:
template<class Y>
explicit shared_ptr( weak_ptr<Y> const & r ): pn( r.pn )
對於第二個問題,假設我在類里儲存了一個指向自身的shared_ptr,那么這個 shared_ptr的計數最少都會是1,也就是說,這個對象將永遠不能析構,所以這種做法是不可取的。
在enable_shared_from_this類中,沒有看到給成員變量weak_this_初始化賦值的地方,那究竟是如何保證weak_this_擁有着Test類對象的指針呢?
首先我們生成類T時,會依次調用enable_shared_from_this類的構造函數(定義為protected),以及類Test的構造函數。在調用enable_shared_from_this的構造函數時,會初始化定義在enable_shared_from_this中的私有成員變量weak_this_(調用其默認構造函數),這時的weak_this_是無效的(或者說不指向任何對象)。
接着,當外部程序把指向類Test對象的指針作為初始化參數來初始化一個shared_ptr(boost::shared_ptr
現在來看看 shared_ptr是如何初始化的,shared_ptr 定義了如下構造函數:
template<class Y>
explicit shared_ptr( Y * p ): px( p ), pn( p )
{
boost::detail::sp_enable_shared_from_this( this, p, p );
}
里面調用了 boost::detail::sp_enable_shared_from_this :
template< class X, class Y, class T >
inline void sp_enable_shared_from_this( boost::shared_ptr<X> const * ppx,
Y const * py, boost::enable_shared_from_this< T > const * pe )
{
if( pe != 0 )
{
pe->_internal_accept_owner( ppx, const_cast< Y* >( py ) );
}
}
里面又調用了enable_shared_from_this 的 _internal_accept_owner :
template<class X, class Y> void _internal_accept_owner( shared_ptr<X> const * ppx, Y * py ) const
{
if( weak_this_.expired() )
{
weak_this_ = shared_ptr<T>( *ppx, py );
}
}
而在這里,對enable_shared_from_this 類的成員weak_this_進行拷貝賦值,使得weak_this_作為類對象 shared_ptr 的一個觀察者。
這時,當類對象本身需要自身的shared_ptr時,就可以從這個weak_ptr來生成一個了:
shared_ptr<T> shared_from_this()
{
shared_ptr<T> p( weak_this_ );
BOOST_ASSERT( p.get() == this );
return p;
}
從上面的說明來看,需要小心的是shared_from_this()僅在shared_ptr
說明1:
所以,如下代碼是錯誤的:
class D:public boost::enable_shared_from_this<D>
{
public:
D()
{
boost::shared_ptr<D> p=shared_from_this();
}
};
原因是在D的構造函數中雖然可以保證enable_shared_from_this
說明2:
如下代碼也是錯誤的:
class D:public boost::enable_shared_from_this<D>
{
public:
void func()
{
boost::shared_ptr<D> p=shared_from_this();
}
};
void main()
{
D d;
d.func();
}
原因同上。
總結為:不要試圖對一個沒有被shared_ptr接管的類對象調用shared_from_this(),不然會產生未定義行為的錯誤。
基於boost.Asio的異步socket例子:
https://github.com/goyas/recipes/tree/master/socket_benchmark
參考文獻:
https://www.jianshu.com/p/4444923d79bd
https://blog.csdn.net/veghlreywg/article/details/89743605
https://www.cnblogs.com/codingmengmeng/p/9123874.html
https://www.cnblogs.com/yang-wen/p/8573269.html