目的
yieldable ( ≈ generative ≈ enumerable ) な task ( ≈ functors ) システムを C++14 と boost::coroutines と boost::signals2 で簡単に実装できて便利に使えるので紹介したい。
補助説明
- yieldable: C# の
yield return
のように関数の中間段階で関数の外に結果を渡せる仕組み。表現として yield から連想しやすい事からこの記事では yieldable とした。プログラミング言語一般論としてはジェネレーターと呼ばれる事もある機能。 boost::coroutines により使用可能。 - task system: 様々な処理内容に対応可能な functor 群の実行を管理してくれる機能としてこの記事では task system と表現した。 functor は C++ においては lambda expression や operator() を実装したユーザー定義型による関数オブジェクト、Cスタイルの関数ポインターなど関数呼び出しとして振る舞う事のできるオブジェクトの意味。
- C++14: この記事で紹介するソースコードは C++14 以降の言語規格に対応したコンパイラーを使わないとそのままでは翻訳できない。C++14の恩恵として lambda expression の引数での
auto
によるテンプレート化、それによるコードの記述の簡略化などの恩恵を受けている。 - boost::signals2: C++ では Qt などでお馴染みの signal/slot 機能を実現するための boost の実装。今回は複数の task を slot として task system の signal に持たせる事で簡単な task system の実装として使っている。スレッドセーフな点も便利で安心して使える。
yieldable task system
- yieldable_task_system.hxx:
# include <boost/coroutine/all.hpp>
# include <boost/signals2/signal.hpp>
# include <memory>
namespace usagi
{
class yieldable_task_system_type
{
boost::signals2::signal< auto () -> void > signal;
public:
template < typename COROUTINE_TYPE >
auto push( COROUTINE_TYPE&& coroutine ) -> void
{
push( boost::coroutines::coroutine< void >::pull_type( std::move( coroutine ) ) );
}
auto push( boost::coroutines::coroutine< void >::pull_type&& coroutine ) -> void
{
auto shared_coroutine = std::make_shared< boost::coroutines::coroutine< void >::pull_type >( std::move( coroutine ) );
auto shared_connection = std::make_shared< boost::signals2::connection >();
*shared_connection = signal.connect
( [ = ]
{
if ( not (*shared_coroutine)() )
shared_connection->disconnect();
}
);
}
auto consume_one() -> bool
{
signal();
return not signal.empty();
}
auto consume_all() -> void
{
while ( consume_one() );
}
auto operator()() -> void
{
consume_all();
}
};
}
使用例
- main.cxx:
# include "yieldable_task_system.hxx"
# include <chrono>
# include <iostream>
auto main() -> int
{
using namespace std::chrono;
usagi::yieldable_task_system_type yieldable_task_system;
yieldable_task_system.push
( [ t0 = high_resolution_clock::now() ]
( auto& yield )
{
std::cout << 'a' << std::flush;
yield();
do
{
std::cout << 'b' << std::flush;
yield();
}
while ( high_resolution_clock::now() - t0 < microseconds( 512 ) );
std::cout << 'c' << std::flush;
yield();
}
);
yieldable_task_system.push
( []
( auto& yield )
{
for ( const auto value : { 'X', 'Y', 'Z' } )
{
std::cout << value << std::flush;
yield();
}
}
);
yieldable_task_system.push
( []
( auto& yield )
{
std::cout << '1' << std::flush;
yield();
std::cout << '2' << std::flush;
yield();
}
);
yieldable_task_system();
}
結果
aX1bY2bZbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbc
(註: 処理中に経過時間を観測しているため、実行ごとに結果の b
の出力回数は異なる可能性があります。)