C++
boost.Asio

boost.Asioを半年使っわかったこと

More than 3 years have passed since last update.

C++ Advent Calendar 2015も21日目

もうすぐ メリー・クリスマスですね。

昨日は

IさんC++ AMPでGPGPU!でした

わたしは残念ながらDirectX11が使えるPCを持っていないので AMPは試せないのですが

GPGPUは楽しいですね

わたしは、boost.asioのまとめです。


はじめに


なぜ boost.Asioが必要だったか

お仕事の関係で、お客さんにC++で高速なステートフルサーバを作って欲しい C++で!

という要望が来たので。


boostに頼る理由

C++11、14 と、標準ライブラリが増え、threadも使えるようになった

が、未だネットワーク通信関連は標準ライブラリになっていないため

boost.Asioを採用した

boost.Asioはネットワークに特化したライブラリではなく、非同期I/Oのライブラリで

タイマー、ファイルの非同期等、色々と利用できる

非同期I/Oなので、大抵の場合はマルチスレッド型よりも速く、シングルスレッドなのでロックが少なくなる


Asioのポイント


非同期関数と同期間数

Asioでは、同期関数と非同期関数が用意されている

非同期のasync_readと、同期のread

当然、非同期関数をメインに使っていくことになる


メンバ関数とフリー関数

read、write、async_read、async_write系には、メンバ関数とフリー関数がある

どちらでも問題なく動作するが

恐らく今のコーディングスタイルでは、フリー関数をメインで使うのが良いと思われる

使った限りでは、あまり動作に違いは感じられなかった

(詳しい方、使い分け等ありましたら 教えて下さい)


3つの async_read/async_write

read/write関数は3種類ずつ存在する


ノーマル

read()、async_read()、write()、async_write() が該当する

標準的なread/writeをする。


xxx_at

read_at()、async_read_at()、write_at()、async_write_at() が該当する

read/writeを行うバイト数を指定する

大量データを小分けにして処理したい時に使うと思われる


xxx_until

read_until()、async_read_until()、write_until()、async_write_until()が該当する

指定した文字列が出現するまで read/writeを行う。正規表現可能。

例えば 改行コードで毎にread/writeする場合に便利


ハンドラと寿命

async関数を呼ぶと関数自体は ハンドラを登録し 直ちに終了する

指定された動作が完了したらハンドラが呼び出される(非同期動作)

それ故に関数内で生成したオブジェクトは、ハンドラが実行される時には既に存在していない事になる

その為に shared_ptrを使うと、サンプルにあった

http://qiita.com/YukiMiyatake/items/f4641c54151a18c362f9

で少しカイセツした

要は、ハンドラをラムダで定義し、自分のshared_ptrをキャプチャすると、関数終了時まで残る


コードの可読性

以前にnode.js を使ったことがあったので、読めたが

最初に見たら少し戸惑うかもしれない


async_read

boost::asio::async_read(socket_, boost::asio::buffer(data, data.length()),

[this](boost::system::error_code ec, std::size_t ){
if (!ec) {
boost::asio::async_write(socket_,
boost::asio::buffer(data, data.length()),
[this](boost::system::error_code ec, std::size_t ){
if (!ec){
.....
}else{
//writeのエラー処理
}
});
}
}else{
// readのエラー処理・・
}
});

非同期処理をすると、上記のような えらいコードが出来上がってしまう

それを避けるために色々な手法が存在する

恐らくは、ある程度の規模の開発だと futureかcoroutineを使う事がベストだと思われる

futureはnode.jsでもあったし同じようなものだと思う

今回は 次期標準ライブラリ候補というcoroutineを使用した


coroutine

最近いろいろな言語で実装されている、軽量スレッド。

どうやら次期C++ではコルーチンも標準化されそう。

なので、既に標準化されたfutreではなくcoroutineを試した

コルーチンはstackles_coroutineとstackful_coroutineの2種類あり、簡単なstacklessから試した


stackles_coroutine

中身を見たら、switch caseのマクロであった

ラベルを使い、ステートマシンで処理を切り替える原始的な方法

ものすごく軽いでしょう Jumpしてるだけなので

ただし、名前のとおり、他のコルーチンとスタックを共有しているので

ローカル変数が使いにくいし、例外のキャプチャ等も面倒そうなので本

大きなシステムには向かないと思ったので 不採用

小さいコードには非常に便利


stackles

reenter(this){

do{
yield acceptor_->async_accept(socket_,*this);
fork server(*this)();
}while(is_parent());

yield socket_.async_read_some(asio::buffer(buffer_), *this);
yield socket.async_write_some(asio::buffer(buffer_), *this);
// ... ここに処理をかいていく
};



stackful_coroutine

これは強力であった。非同期の 悪魔のような }); が無くなった

コルーチン毎にstackを生成するので、マルチスレッドと同じ感覚で書ける

しかも、シングルスレッドで動作することが保証されるため

面倒な排他のことを考える必要もない!

asioを使う際には 一度stackful_coroutineを試してみる事を強くおすすめする

コードは


stackful

boost::asio::spawn(strand_, [this](boost::asio::yield_context yield){

async_read(socket_, asio::buffer(buffer_), yield);
async_write(socket_, asio::buffer(buffer_), yield);
// ... ここにコードを追加する
});

stacklesでも同じだが、async_xxx と書いても、見かけ上は 同期しているように見える

とても人間にわかりやすい

asio::spawnを呼ぶと、コルーチンを作成する。

第一引数は おなじみio_serviceで、第二引数にコルーチンが生成された時の関数を登録する

実際には async_acceptをした際に spawnをする流れになるだろう

簡単に動作を説明すると、async_xxx を呼んだ時に、自主的に軽量タスクを解放する(ノンプリエンプティブ)

他にシグナルになっている軽量タスクがあれば、そこにタスクスイッチする。

stackfulの場合は、軽量タスクがそれぞれにスタックフレームを持っているため、ローカル変数等も使うことが可能


例外処理

通常、stackful_coroutineでは例外が飛んで来るが、yieldを yield[boost::system::error_code] にすると、例外を発生させずに

エラーコードが返ってくる

必要に応じて使い分けられるので便利


タイムアウト

これは少し苦労した。

流れとしては

asio::deadline_timer を作成し

asio::deadline_timer.expires_from_now にタイムアウト時間を設定すし

asio::deadline_timer.async_wait を呼ぶと、タイムアウトorキャンセル時にハンドラが呼ばれるが

この後、例えばread処理を行い、readが終わればtimerをキャンセルする。

もしreadされる前に タイムアウトした場合は タイムアウトハンドラが呼ばれるので、そこでsocketをキャンセル

非常に 面倒な処理である

これが正しいのかわからないが 現状正しく動作しているので、これで行っている


timeout

asio::deadline_timer timer; 

timer.expires_from_now(boost::posix_time::seconds(10));
timer.async_wait(
[&socket_](const boost::system::error_code &ec) {
if (ec == boost::asio::error::operation_aborted) {
// キャンセルされた場合
cout << cancel << endl ;
} else {
// タイムアウト時はソケットをキャンセルし readをやめる
socket_.cancel();
cout << timeout << endl ;
}
});
async_read(socket_, boost::asio::buffer(buffer_), yield);
// readが先におわったので、タイマーをキャンセル
timer.cancel();

最後の async_readをタイムアウト付きで呼びたい。今回は10秒待って データが来なければタイムアウトする

まず deadline_timerを作成し10秒に設定する

その後 タイマーのasync_waitで、タイムアウト時のハンドラを登録する。

その後 async_readを行なう。


  1. 時間内にasync_readが成立した場合(ノンタイムアウト)

    async_readが終わり、timer.cancel() が実行される

    タイマーのハンドラが operation_aborted で呼ばれる


  2. 時間内にasync_readが成立しない(タイムアウト)

    タイマーハンドラが timed_out で呼ばれる

    socket.cancel() で、async_readを終了させる

    (timer.cancelが呼ばれるが それが気持ち悪いなら フラグで呼ばないよう制御すればいい)


async_readにタイムアウト設定出来るオーバーロードが欲しいと思った

(無いので 関数作った)


イベントハンドラ

Asioはネットワーク通信として主に使われるが

非同期I/O のライブラリなので、ネットワーク以外にも使える

例えば 1秒毎に Update関数を呼ぶ無限ループは


イベントハンドラ

boost::asio::spawn(io_service_, [&self](boost::asio::yield_context yield) {

for (; ;) {
timer.expires_from_now(boost::posix_time::seconds(1));
timer.async_wait(yield[ec]);

Update();
}
});


こんな形で簡単に実装できる

もっとちゃんとしたサンプルは、高橋晶さんの

http://faithandbrave.hateblo.jp/entry/20110530/1306739714

このあたりを。

ネットワーク通信ではイベント処理的に処理する事も多いので

そういう場合にも役に立ちます


まとめ

とりあえず、この程度の知識で、実務で耐えうる非同期サーバを構築する事が可能

シングルスレッドなので、慣れれば マルチスレッド方式より楽に作れると思う

大規模なシステムであればcoroutineかfutureを使うことをおすすめ

同期関数を使って従来のマルチスレッド方式も出来るんだよ

思ったより素直なライブラリなので、わりと簡単に扱えると思います


番外編:Asioするうえで便利だったもの


ラムダ

非同期ライブラリを使うにはラムダがとにかく便利。いちいち関数オブジェクト作りたくない


functional、bind

ラムダを使えば必ずセットに出てくるもの。


shared_ptr

非同期ゆえに、オブジェクトの生存期間が難しいので、shared_ptrは重宝します


tuple、tribool

webプログラミングをしていると、ステータスコードとメッセージが欲しいとか

データ取得時に true、false以外にも まだバッファ残っているという状態が欲しかったり

普段のコード以上に tupleやtriboolが便利だと思った


 副作用

永遠の課題かもしれない。

Asioに限らず、副作用を減らす事は保守性にも関わるし

遅延評価など高速化にも関わるはずで

特に非同期コードを書いていると、副作用が多いと苦労した


さいごに

つぎは22日目 hgodaiさん

お楽しみに!