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 を使ったことがあったので、読めたが
最初に見たら少し戸惑うかもしれない
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してるだけなので
ただし、名前のとおり、他のコルーチンとスタックを共有しているので
ローカル変数が使いにくいし、例外のキャプチャ等も面倒そうなので本
大きなシステムには向かないと思ったので 不採用
小さいコードには非常に便利
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を試してみる事を強くおすすめする
コードは
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をキャンセル
非常に 面倒な処理である
これが正しいのかわからないが 現状正しく動作しているので、これで行っている
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を行なう。
-
時間内にasync_readが成立した場合(ノンタイムアウト)
async_readが終わり、timer.cancel() が実行される
タイマーのハンドラが operation_aborted で呼ばれる -
時間内に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さん
お楽しみに!