Boost.Asioで非同期操作中に*thisの寿命が尽きることを防ぐ
Boost.Asioを使って非同期操作を書いていると、メンバ関数から起動した非同期操作の最中に*thisの寿命が尽きて困ることがあります。
例えば次のHTTPクライアント:
class http_client { public: // ... void start() { // ... // 名前解決を開始する m_resolver.async_resolve( query, std::bind(&http_client::handle_resolve, this, _1, _2)); } private: // ... // リクエスト書き込みが終了したときに呼ばれる void handle_write( error_code const &error, std::size_t bytes_transferred) { // ... // レスポンスを読み込む asio::async_read( m_socket, m_streambuf, std::bind(&http_client::handle_read, this, _1, _2)); } // レスポンス読み込みが終了したときに呼ばれる void handle_read( error_code const &error, std::size_t bytes_transferred) { // ... } asio::streambuf m_streambuf; };
このコードに潜在するリスクの1つは、asio::async_read(m_socket, ...)によって開始される非同期操作中に、*thisの寿命が尽きてしまう可能性があることです。その場合、バッファ(m_streambuf)が読み込み中に破壊されたり、ハンドラ内で既に破壊されたメンバ変数にアクセスしたりして、意図しない動作が引き起こされます(dangling reference)。
この問題は、http_clientクラスのデストラクタで非同期操作をキャンセルするだけでは解決しません。socket::cancel()を呼び出しても、既にio_serviceにpost()されたハンドラは実行されますし、cancel()はハンドラの終了を待ってくれません(I/O objectのcancel()はそういう動作をします)。
ユーザーとの間で、非同期操作の終了まで*thisを破棄しないという同意が何らかの形で成立していればいいですが、できればクラスの中だけでこの問題を解決したいものです。
*thisをshared_ptrで管理する
この問題を解決するよく知られた方法は、*thisをshared_ptrで管理するようにし、ハンドラに*thisを指すshared_ptrを"埋め込む"ことで、非同期操作が終了するまで*thisを延命する方法です。具体的には、enable_shared_from_thisを用います。
class http_client : public std::enable_shared_from_this<http_client> { public: // ... private: // ... void handle_write( error_code const &error, std::size_t bytes_transferred) { // ... asio::async_read( m_socket, m_streambuf, // ハンドラに*thisを指すshared_ptrを埋め込む std::bind(&http_client::handle_read, shared_from_this(), _1, _2)); } void handle_read( error_code const &error, std::size_t bytes_transferred) { // ... } asio::streambuf m_streambuf; };
この方法の欠点は、クラスのインスタンスを常にshared_ptrで管理することをユーザーに強制する点です。
非同期操作で使うメンバ変数が延命できればそれでいい場合は、そのメンバ変数のみshared_ptrで管理するという手もありますが、*thisそのものは延命できません。
class http_client { public: // ... private: // ... void handle_write( error_code const &error, std::size_t bytes_transferred, std::shared_ptr<asio::ip::tcp::socket> const &socket) { // ... // バッファをメンバ変数でなく、shared_ptr型のローカル変数として定義する auto const streambuf = std::make_shared<asio::streambuf>(); asio::async_read( *socket, *streambuf, // ハンドラにバッファを指すshared_ptrを埋め込む std::bind(&http_client::handle_read, this, _1, _2, socket, streambuf)); } void handle_read( error_code const &error, std::size_t bytes_transferred, std::shared_ptr<asio::ip::tcp::socket> const &socket, std::shared_ptr<asio::streambuf> const &streambuf) { // ... } // asio::streambuf m_streambuf; };
この方法は、非同期操作で使うメンバ変数を局在化するために用いることもできます(Boost.Asio 送受信バッファをメンバ変数に持たないためのイディオム - Faith and Brave - C++で遊ぼう)。
デストラクタでwaitする
非同期操作が終了するまで待つwait()を用意しておけば、デストラクタでwait()を呼ぶことで、非同期操作中の*this破壊を回避することができます。実装としてはpromise/futureを使うのが良さそうです。
class http_client { public: // ... ~http_client() { cancel(); wait(); } void start() { // ... // promiseを作成する auto const promise = std::make_shared<std::promise<void>>(); // futureをメンバ変数に保存する m_futures.push_back(promise->get_future()); m_resolver.async_resolve( query, // promiseをハンドラに埋め込む std::bind(&http_client::handle_resolve, this, _1, _2, promise)); } // 全ての非同期操作が終了するまでブロックする関数 void wait() { // futureのshared stateがreadyになるのを待つ for (auto &future : m_futures) future.wait(); } private: // ... void handle_write( error_code const &error, std::size_t bytes_transferred, std::shared_ptr<std::promise<void>> const &promise) { // ... asio::async_read( m_socket, m_streambuf, // promiseを次のハンドラに渡す std::bind(&handle_read, this, _1, _2, promise)); } void handle_read( error_code const &error, std::size_t bytes_transferred, std::shared_ptr<std::promise<void>> const &promise) { // ... } // 非同期操作が終了した時点で、 // ハンドラに埋め込まれていたpromiseが破壊され、 // shared stateがreadyになる std::vector<std::future<void>> m_futures; asio::streambuf m_streambuf; };
一連のハンドラ呼び出しが終了した時点で、ハンドラに埋め込まれていたpromiseは破壊され、メンバ変数のfutureが参照しているshared stateがreadyになります(30.6.5/7、30.6.4/7)。wait()はfuture::wait()を呼び出してこれを待ちます。