84
79

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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さん

お楽しみに!

84
79
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
84
79

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?