Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

9,052 views

Published on

Boost.Asio を読みやすくする。

Published in: Engineering
  • Hello there! Get Your Professional Job-Winning Resume Here! http://bit.ly/topresum
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

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

  1. 1. Boost.Asioに可読性を求めるのは 間違っているだろうか 株式会社ムラサメ研究所 宮竹ゆき
  2. 2. 自己紹介 株式会社ムラサメ研究所(税金対策会社)主神 会社名はノリでつけました。 普通にエンジニアです。ファミリアメンバー(社 畜)募集中! ゲーム系、低レイヤー系、カーネル系 等が得意分野です C++はSTLやTemplateは今まで使ってきませんでし た 完全に食わず嫌いです constexprが読めない 大都会岡山の恥さらしです 三度の飯より TKG(卵かけごはん)が好き
  3. 3. はじめに  boost 1.57.0を前提にすすめます(1.58 でも動作確認はしたが)  Cmake clang++ でビルドしてます  MacとLinuxで確認していますが、マ ルチプラットフォームなはず  C++14歴1ヶ月未満の初心者です  サンプルのソースは動作確認してな いので参考まで
  4. 4. Asioを使う事になった理由  速度が要求されるサーバの案件 元のソースが bind(…); listen(…); accept(…); begin_thread(…); マルチスレッド型では同時接続数を増 やすの難しい! そうだ boost.Asioにしよう!
  5. 5. Boost.Asioとは  Asynchronous I/O。proactorパターン  シングルスレッドでI/Oを非同期で処理する  従来のmultithreadパターンだと、threadのコストが高 く、大量のクライアントを さばけない(C10K問題)  カーネルにread/write命令を送り、完了時にコール バックさせるselect/epoll等のnonblocking I/Oと比較し て、user/kernelのコンテキストスイッチが少なくなる と思われる LinuxではepollでProacotr実装している模様  関数型プログラミング勉強してみたかったので、コー ルバック地獄は本望だ  ラムダたん
  6. 6. 実例 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のエラー処理・・ } }); はようコールバックまみれになろうぜ
  7. 7. 可読性を上げる方法  コールバックを自力でなんとかする coroutineを使う  promise/futureを使う promise/futureは難しそうなので 今回はcoroutineを使う事に
  8. 8. coroutineとは  Fiber、micro thread等色々あるが、 軽量スレッドの事  疑似並列化を行う事が可能(協調的、 ノンプリエンプティブ)  シングルスレッドなので同期問題が 起こりにくい  Threadと違い、自分でCPUを開放す る(協調的)
  9. 9. stackless coroutine  簡単そうだったのでこちらから  asio::coroutineを継承する  適用範囲は #include <boost/asio/yield.hpp> ~ #include <boost/asio/unyield.hpp> で囲む  coroutine関数は operator()に operator()以外でも出来そうだけど調べてない  coroutine部分は reenter()で囲む  yield fork等をキーワードのように使え る
  10. 10. 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_;
  11. 11. operator()、reenter  コルーチンが再開される場所  CPUが割り当てられた際に、登録した 関数オブジェクトハンドラが呼ばれる  reenter内の前回中断した場所から再開 される void server::operator()(boost::system::error_co de ec, std::size_t length) { if (!ec) { reenter(this){
  12. 12. 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());
  13. 13. 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);
  14. 14. 例 // 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); }; 可読性あがりましたよね?
  15. 15. メリット  非常に簡単お手軽に書く事が出来る  かなり軽そう(switch caseで実装さ れてる)  コールバック地獄がなくなり、一見 直列処理になり、ソースの可読性も 非常に良くなった  ヘッダの追加のみで ライブラリの追 加が不要
  16. 16. 問題点  switch caseで実装されているため、 ローカル変数の扱いが制限される ローカル変数の扱い方が難しくなるた め、使用を辞めた
  17. 17. 参考  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
  18. 18. stackful coroutine  依存ライブラリ ./b2 --with-system --with-thread --with- date_time --with-regex --with-serialization  asio::spawn()に、コルーチンを行いたい関数 (オブジェクト)を指定しyield_contextを取得 する  Stacklessと違い、スタックコンテキストを個 別に作成するので、ローカル変数も可能  Asynchronous命令のコールバックに yield_contextを指定する 意外に簡単そう
  19. 19. 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 を使う! }); } }
  20. 20. 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);
  21. 21. 例外処理  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]); 老害は速度の速いエラーコード方式が好き
  22. 22. 例 // 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); }); 可読性あがりましたよね?
  23. 23. 参考  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
  24. 24. 有限状態マシン(finite state machine)  通信に限らずStateパターンを使う事は 多いが、大抵はこの部分の可読性が悪 くなる  一般的にはenumでモードを設定しint型 と相互変換して使ったり、switch caseあ るいは関数ポインタ配列で実装したり  State変更関数作ったり・・  大抵は Stateがネストしたり、State変更 条件が色々あったり・・・ それらが大幅に簡略化されます
  25. 25. Boost.MSM  有限状態マシン(finite state machine)  Boost.Statechartより速い(RTTIや virtualを使用してないっぽい)  Boost::MPLを使う  StateとEventを作成する  transition_tableを作成し、Eventにより Stateを変更できる  ガード、アクションを指定出来る
  26. 26. 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へ遷 移
  27. 27. Boost.MSM  Transition_tableに上限がある mpl::vectorを使っているので BOOST_MPL_LIMIT_VECTOR_SIZE を指定する  コンパイル時間がかかる pImplイデオム使おう  コンパイル時にステートが決まってる必要がある 動的にステートを作らなければOK  遷移の拒否(Guard)が出来る UML2準拠らしい  遷移時のアクションを指定できる Stateでon_enter()、on_exit()をオーバーライド出来る が、それ以外に transition_tableに関数オブジェクトを登 録できる  複雑な状態遷移がある場合にはとても便利
  28. 28. boost.Asio Tips
  29. 29. 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をキャプチャし、ハンドラ終了までオブジェクトを 終了させない
  30. 30. 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(); これで正しいのかわかりませんが、現状動いています
  31. 31. 総括  マルチスレッド型は大量クライアントをさばけない(C10K)  マルチスレッド型だとデータの同期処理にコストがかかる ので、非同期 I/Oを使うべき  非同期I/Oを普通に書くとコールバック地獄に陥る  coroutineを使うと、直列っぽく記述出来るので可読性が上 がる  stackless_coroutineはとてもコストが低そうだが、switchを 使ったマクロなため、ローカル変数等の制限が発生する  stackful_coroutineは制限もなくとても便利である!  ステートマシンは Boost.Statechartあるいは Boost.MSMを使 えばよい  C++のジェネリックラムダが来ると更に快適になりそうだ

×