C++でアクターモデルを実現するためのライブラリをシングルヘッダファイルで作成しました。
アクターモデルと聞くとムズカシイ印象を受けるかもしれませんが、仕組みを実現するだけならそんなに難しくない、ということが伝える目的で書いてみました。
ポイントは以下のとおり。
- C++の標準ライブラリのみで実装(thread,chrono,mutex,atomic,queue)
- 性能よりも構成要素の小ささを優先
アクターモデルとは
アクターモデルは並列計算の数学的モデルの一種です。並列処理指向と言われるErlangや関数型言語Scalaでも使われています。
ざっくりとですが特徴をあげておきます。
- あるアクターオブジェクトは同時に高々一つの(スレッド、プロセス、PC)上で動作する。
- アクター同士はメッセージによる非同期通信を行う。
- アクターは個別にメッセージボックスを持っている。
- アクターはメッセージの送受信でのみ他のアクターとデータのやり取りを行う。
以下、Wikipediaより引用。
アクターモデル(英: actor model)とは、1973年、カール・ヒューイット、Peter Bishop、Richard Steiger が発表した並行計算の数学的モデルの一種[1]。アクターモデルでは、並行デジタル計算の汎用的基本要素として「アクター」という概念を導入している。アクターモデルは並行性の理論的理解のフレームワークとして使われるほか、並行システムの実装の理論的基礎としても利用されてきた。
[https://ja.wikipedia.org/wiki/%E3%82%A2%E3%82%AF%E3%82%BF%E3%83%BC%E3%83%A2%E3%83%87%E3%83%AB]
アクターモデルは何が嬉しいのか
個人的に感じるアクターモデルの利点を書いておきます。
- アクターの受け取ったメッセージに対する処理は逐次的に記載できる。(メッセージボックスへのデータ入出力部などは同期処理が必要だが容易に隠蔽できる)
- 同期処理による不必要な待ち時間を減らすことができる。
- ロックをほとんど使わないのでデッドロックの心配がない。
- メッセージ以外の入力がないため処理が把握しやすい。
「MiniActor」
アクターモデルに基づいたプログラムを実現するために作成したシングルヘッダライブラリです。
VC2015,GCC4.8.1,clang3.4で動作確認済み。
できること
- C++標準ライブラリのみの使用による、他の外部ライブラリを用いない導入
- スレッド単位のアクター動作
- 任意の型によるメッセージ送受信(Template引数によるメッセージの型の指定)
- スレッドを立てないメッセージ送受信(GUIスレッドなどで利用)
- Two-Phase Termination(スレッドの停止→オブジェクト破棄)
できないこと
- 高速処理(性能より読みやすさを重視)
- たくさんのアクターを立てる(OSのスレッド数に依存)
- 高度な例外処理(例外に対してはほぼ無考慮)
使い方
MiniActorの使い方例です。
Example01 : 文字列を送る
文字列を受け取って、標準出力に出力するアクター(EchoActor)の例です。
- MiniActor::Actorを継承したアクタークラスを定義します。そのさい、テンプレート引数にはメッセージの型を指定してください。
- 受け取ったメッセージの処理内容はprocess_message関数に定義します。
- アクターにメッセージを送るには、send関数を利用してください。
- アクターを停止するときは、halt関数を実施してください。アクターは終了状態となり、メッセージ処理を終了して自身のスレッドを停止します。
include <iostream>
#include <string>
#include <MiniActor.hpp>
struct EchoActor : public MiniActor::Actor<std::string>
{
void process_message(const Message& msg)
{
std::cout << "received message: " << msg << std::endl;
}
};
int main()
{
EchoActor actor;
actor.send("Test");
actor.send("Hello");
actor.send("World");
std::this_thread::sleep_for(std::chrono::seconds(1));
actor.halt();
return 0;
}
Example02 : アクター同士での通信
2つのアクターがそれぞれ受け取った値をインクリメントし、相手側に返す例です。
#include <iostream>
#include <string>
#include <MiniActor.hpp>
struct CountActor;
using SpCountActor = std::shared_ptr<CountActor>;
using WpCountActor = std::weak_ptr<CountActor>;
struct CountActor : public MiniActor::Actor<std::pair<int,WpCountActor> >, std::enable_shared_from_this<CountActor>
{
void process_message(const Message& msg)
{
std::cout << std::this_thread::get_id() << " : " << msg.first << std::endl;
SpCountActor sp = msg.second.lock();
if (sp) {
sp->send(std::make_pair(1 + msg.first, shared_from_this()));
}
}
};
int main()
{
SpCountActor actor1 = std::make_shared<CountActor>();
SpCountActor actor2 = std::make_shared<CountActor>();
actor1->send( std::make_pair(0, actor2));
std::this_thread::sleep_for(std::chrono::seconds(1));
actor1->halt();
actor2->halt();
return 0;
}
後で書き足したいモノ
- メインスレッドでメッセージを受け取る例
- Boost.Variantを利用して、メッセージを型で定義する例
- Boost.MSMを利用して、アクターの状態遷移を定義する例