More than 1 year has 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さん

お楽しみに!