SlideShare a Scribd company logo
Boost.Asioに可読性を求めるのは
間違っているだろうか
株式会社ムラサメ研究所
宮竹ゆき
自己紹介
株式会社ムラサメ研究所(税金対策会社)主神
会社名はノリでつけました。
普通にエンジニアです。ファミリアメンバー(社
畜)募集中!
ゲーム系、低レイヤー系、カーネル系
等が得意分野です
C++はSTLやTemplateは今まで使ってきませんでし
た
完全に食わず嫌いです
constexprが読めない 大都会岡山の恥さらしです
三度の飯より TKG(卵かけごはん)が好き
はじめに
 boost 1.57.0を前提にすすめます(1.58
でも動作確認はしたが)
 Cmake clang++ でビルドしてます
 MacとLinuxで確認していますが、マ
ルチプラットフォームなはず
 C++14歴1ヶ月未満の初心者です
 サンプルのソースは動作確認してな
いので参考まで
Asioを使う事になった理由
 速度が要求されるサーバの案件
元のソースが
bind(…);
listen(…);
accept(…);
begin_thread(…);
マルチスレッド型では同時接続数を増
やすの難しい!
そうだ boost.Asioにしよう!
Boost.Asioとは
 Asynchronous I/O。proactorパターン
 シングルスレッドでI/Oを非同期で処理する
 従来のmultithreadパターンだと、threadのコストが高
く、大量のクライアントを さばけない(C10K問題)
 カーネルにread/write命令を送り、完了時にコール
バックさせるselect/epoll等のnonblocking I/Oと比較し
て、user/kernelのコンテキストスイッチが���なくなる
と思われる
LinuxではepollでProacotr実装している模様
 関数型プログラミング勉強してみたかったので、コー
ルバック地獄は本望だ
 ラムダたん
実例
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のエラー処理・・
}
});
はようコールバックまみれになろうぜ
可読性を上げる方法
 コールバックを自力でなんとかする
coroutineを使う
 promise/futureを使う
promise/futureは難しそうなので
今回はcoroutineを使う事に
coroutineとは
 Fiber、micro thread等色々あるが、
軽量スレッドの事
 疑似並列化を行う事が可能(協調的、
ノンプリエンプティブ)
 シングルスレッドなので同期問題が
起こりにくい
 Threadと違い、自分でCPUを開放す
る(協調的)
stackless coroutine
 簡単そうだったのでこちらから
 asio::coroutineを継承する
 適用範囲は #include
<boost/asio/yield.hpp> ~ #include
<boost/asio/unyield.hpp> で囲む
 coroutine関数は operator()に
operator()以外でも出来そうだけど調べてない
 coroutine部分は reenter()で囲む
 yield fork等をキーワードのように使え
る
header
 asio::coroutineを継承する事
class server : boost::asio::coroutine {
 コールバック先の関数オブジェクト
void operator()(ほげ)
 fork時にコンストラクタが呼ばれないので、
shared_ptr等を使うと初期化しやすい(指摘あれ
ばよろしくお願いします)
std::shared_ptr<tcp::acceptor> acceptor_;
std::shared_ptr<tcp::socket> socket_;
std::shared_ptr<std::array<char, 1024> > buffer_;
operator()、reenter
 コルーチンが再開される場所
 CPUが割り当てられた際に、登録した
関数オブジェクトハンドラが呼ばれる
 reenter内の前回中断した場所から再開
される
void
server::operator()(boost::system::error_co
de ec, std::size_t length) {
if (!ec) {
reenter(this){
fork
 forkを使うと新たな疑似コンテキストを作成で
きる
 is_parentで親子が判定出来る
 新たなコンテキストは、個別のメンバを割り当
てられる(が コンストラクタはforkでは呼ばれ
ない)
do {
socket_.reset(new tcp::socket(acceptor_->get_io_service()));
yield acceptor_->async_accept(*socket_, *this);
fork server(*this)();
} while (is_parent());
yield
 非同期処理をpostし、CPUを開放する
 非同期処理完了時に、指定した関数オ
ブジェクトがコールバックされる
yield
socket_>async_read_some(boost::asio::buff
er(*buffer_), *this);
yield boost::asio::async_write(*socket_,
boost::asio::buffer(*buffer_), *this);
例
// coroutineを使わない例
asio::async_read(socket_, asio::buffer(buffer_),
[this](boost::system::error_code ec, std::size_t ){
asio::async_write(socket_, asio::buffer(buffer_),
[this](boost::system::error_code ec, std::size_t ){
socket_.shutdown(tcp::socket::shutdown_both, ec);
});
}
});
// stackless_coroutineを使った例
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);
socket_.shutdown(tcp::socket::shutdown_both, ec);
};
可読性あがりましたよね?
メリット
 非常に簡単お手軽に書く事が出来る
 かなり軽そう(switch caseで実装さ
れてる)
 コールバック地獄がなくなり、一見
直列処理になり、ソースの可読性も
非常に良くなった
 ヘッダの追加のみで ライブラリの追
加が不要
問題点
 switch caseで実装されているため、
ローカル変数の扱いが制限される
ローカル変数の扱い方が難しくなるた
め、使用を辞めた
参考
 stackless coroutine Overview
http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/overvie
w/core/coroutine.html
 AsioSample HTTP server4
http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/exampl
es/cpp03_examples.html
stackful coroutine
 依存ライブラリ
./b2 --with-system --with-thread --with-
date_time --with-regex --with-serialization
 asio::spawn()に、コルーチンを行いたい関数
(オブジェクト)を指定しyield_contextを取得
する
 Stacklessと違い、スタックコンテキストを個
別に作成するので、ローカル変数も可能
 Asynchronous命令のコールバックに
yield_contextを指定する
意外に簡単そう
spawn
spawnにより yield_contextを作成する
複数の接続を受ける場合は connection毎に spawnする必要がある
boost::asio::spawn(io_service, [&] (boost::asio::yield_context yield) {
…
for (;;) {
tcp::socket _socket(io_service);
acceptor.async_accept(_socket, yield);
// C++14の汎用ラムダキャプチャ欲しい・・
asio::spawn(strand_, [_socket](asio::yield_context yield_child) {
tcp::socket socket(std::move(_socket));
….. yield_child を使う!
});
}
}
yield
 非同期関数にyield contextを渡す事で、コルーチンに
出来る
acceptor.async_accept(socket_, yield);
// tcp::socketのメンバ関数でもOK
socket_.async_read_some(asio::buffer(buffer_), yield);
// asioのフリー関数でもOK
asio::async_write(socket_, asio::buffer(buffer),yield);
例外処理
 yieldコンテキストを渡す際、通常は例外がthrowされ
るが、operator[]を使えばエラーコード方式に出来る
// 例外throw方式
socket_.async_read_some(boost::asio::buffer(buffer_),
yield);
// エラーコード方式
boost::system::error_code;
ec;socket_.async_read_some(boost::asio::buffer(buffer_),
yield[ec]);
老害は速度の速いエラーコード方式が好き
例
// coroutineを使わない例
asio::async_read(socket_, asio::buffer(buffer_),
[this](boost::system::error_code ec, std::size_t ){
asio::async_write(socket_, asio::buffer(buffer_),
[this](boost::system::error_code ec, std::size_t ){
socket_.shutdown(tcp::socket::shutdown_both, ec);
});
}
});
// coroutineを使った例
boost::asio::spawn(strand_, [this, self](boost::asio::yield_context yield){
boost::system::error_code ec;
socket_.async_read_some(asio::buffer(buffer_), yield[ec]);
socket.async_write_some(asio::buffer(buffer_), yield[ec]);
socket_.shutdown(tcp::socket::shutdown_both, ec);
});
可読性あがりましたよね?
参考
 stackful coroutine Overview
http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/overvie
w/core/spawn.html
 Asio sample spawn
http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/exampl
e/cpp11/spawn/echo_server.cpp
有限状態マシン(finite state machine)
 通信に限らずStateパターンを使う事は
多いが、大抵はこの部分の可読性が悪
くなる
 一般的にはenumでモードを設定しint型
と相互変換して使ったり、switch caseあ
るいは関数ポインタ配列で実装したり
 State変更関数作ったり・・
 大抵は Stateがネストしたり、State変更
条件が色々あったり・・・
それらが大幅に簡略化されます
Boost.MSM
 有限状態マシン(finite state machine)
 Boost.Statechartより速い(RTTIや
virtualを使用してないっぽい)
 Boost::MPLを使う
 StateとEventを作成する
 transition_tableを作成し、Eventにより
Stateを変更できる
 ガード、アクションを指定出来る
Boost.MSM
// 状態作成
struct start : public boost::msm::front::state<>{};
struct end : public boost::msm::front::state<>{};
// イベント作成
struct event_goal{};
// イベントテーブル作成
struct state : public msm::front::state_machene_def<state>
{
struct table : mpl::vector<
_row<start, event_goal, end> // goalイベントが来たら endへ遷移
>
….
}
state st;
st.process_event(goal()); // goalイベント呼ばれたので endへ遷
移
Boost.MSM
 Transition_tableに上限がある
mpl::vectorを使っているので
BOOST_MPL_LIMIT_VECTOR_SIZE を指定する
 コンパイル時間がかかる
pImplイデオム使おう
 コンパイル時にステートが決まってる必要がある
動的にステートを作らなければOK
 遷移の拒否(Guard)が出来る
UML2準拠らしい
 遷移時のアクションを指定できる
Stateでon_enter()、on_exit()をオーバーライド出来る
が、それ以外に transition_tableに関数オブジェクトを登
録できる
 複雑な状態遷移がある場合にはとても便利
boost.Asio Tips
shared_ptrキャプチャ
class session{
void do_read(){
async_read( socket_,buffer,[this](){this->hoge;});
}
}
非同期なのでハンドラ実行時にthisは開放されているかもしれない
ハンドラを実行しようと心の中で思ったならッ!その時スデにオブ
ジェクトは終わってるんだッ!
class session::enable_shared_from_this<session>{
void do_read(){
auto self(shared_from_this());
async_read( socket_,buffer,[this, self](){this->hoge;});
}
}
自分のshared_ptrをキャプチャし、ハンドラ終了までオブジェクトを
終了させない
timeout処理
 coroutine時のtimeoutに一晩うなされた結果、下記のように行いま
した
asio::deadline_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 {
socket_.cancel();
cout << “timeout” << endl ;
}
});
socket_.async_read_some(boost::asio::buffer(buffer_), yield[ec]);
timer_.cancel();
これで正しいのかわかりませんが、現状動いています
総括
 マルチスレッド型は大量クライアントをさばけない(C10K)
 マルチスレッド型だとデータの同期処理にコストがかかる
ので、非同期 I/Oを使うべき
 非同期I/Oを普通に書くとコールバック地獄に陥る
 coroutineを使うと、直列っぽく記述出来るので可読性が上
がる
 stackless_coroutineはとてもコストが低そうだが、switchを
使ったマクロなため、ローカル変数等の制限が発生する
 stackful_coroutineは制限もなくとても便利である!
 ステートマシンは Boost.Statechartあるいは Boost.MSMを使
えばよい
 C++のジェネリックラムダが来ると更に快適になりそうだ

More Related Content

BoostAsioで可読性を求めるのは間違っているだろうか