内部のSObjectizerを見てみましょう

SObjectizerと呼ばれるオープンなC ++フレームワークを読者に紹介し続けます。 このフレームワークは、アクターモデル、CSP、およびパブリッシュ/サブスクライブから借用した高レベルのツールがC ++プログラマーに利用可能になるため、複雑なマルチスレッドアプリケーションの開発を簡素化します。 同時に、どんなに高音であっても、SObjectizerはC ++向けの数少ないオープンで活発な、開発中のアクターフレームワークの1つです。



Habréに関する10以上の記事をすでにSObjectizerに捧げています。 それでも、SObjectizerがどのように機能し、SObjectizerが動作するさまざまなタイプのエンティティが相互に接続されているかを理解する際に、読者は「ホワイトスポット」の存在について不満を述べています。



この記事では、SObjectizerの内部を見て、「指で」試してみて、それが何で構成され、一般的にはどのように機能するかを写真で説明します。



SObjectizer環境



SObjectizer Environment(または略してSOEnv)のようなものから始めましょう。 SOEnvは、SObjectizerに関連するすべてのエンティティ(エージェント、協同組合、ディスパッチャー、メールボックス、タイマーなど)が作成されて動作するコンテナです。次の図に示すことができます。







実際、SObjectizerを使い始めるには、SOEnvのインスタンスを作成して実行する必要があります。 たとえば、この例では、プログラマーはSOEnvのインスタンスをso_5 :: wrapped_env_t型のオブジェクトとして手動で作成します。



int main() { so_5::wrapped_env_t sobj{...}; ... // -   . return 0; }
      
      





このインスタンスはすぐに動作を開始し、so_5 :: wrapped_env_tオブジェクトが破棄されると自動的にシャットダウンします。



同じアプリケーション内で互いに独立したSObjectizerの複数のインスタンスを実行できるようにするためには、SOEnv自体の本質が別の概念として必要でした。



 int main() { so_5::wrapped_env_t first_soenv{...}; so_5::wrapped_env_t second_soenv{...}; ... so_5::wrapped_env_t another_soenv{...}; ... // -   . return 0; }
      
      





これにより、アプリケーションでこの画像を取得できます。





楽しい事実。 最も近い、はるかに人気のある競合他社であるC ++アクターフレームワーク(別名CAF)は、少し前までは、アプリケーションで1つのアクターサブシステムしか実行できませんでした。 そして、CAF開発者にその理由を尋ねる議論に出くわしました。 しかし、時間の経過とともに、アクターシステムの概念と、アプリケーションで複数のアクターシステムを同時に起動する機能がCAFに登場しました。

SObjectizer Environmentは何に責任がありますか?



SOEnvは、協同組合、ディスパッチャなどを格納するコンテナであるという事実に加えて、SOEnvはこれらのエンティティも管理します。



たとえば、SOEnvを開始するときは、次を開始する必要があります。





したがって、SOEnvが停止した場合、これらのエンティティはすべて停止する必要があります。



また、ユーザーがエージェントをSOEnvに追加する場合、SOEnvはユーザーエージェントとの連携のために登録手順を完了する必要があります。 また、ユーザーがエージェントを削除する場合、SOEnvは協力を登録解除する必要があります。



SOEnvには2つの主要なリポジトリがあり、その内容を所有しています。 SObjectizerの次のメジャーバージョンで完全に消える可能性のある最初のリポジトリは、パブリックディスパッチャのリポジトリです。 各パブリックディスパッチャには、独自の一意の文字列名が必要です。これにより、ディスパッチャを見つけて再利用できます。



最も重要な2番目のリポジトリは、連携リポジトリです。 各協同は独自の固有の文字列名も持っている必要があり、その下に協同が協同リポジトリに保存されます。 既に取得した名前でパートナーシップを登録しようとすると失敗します。





協同組合での名前の存在は、おそらくSObjectizer-4から継承された初歩でしょう。 現在、協力の名前はかなりあいまいな機能とみなされており、SObjectizerでの協力は時間の経過とともに匿名になる可能性があります。 しかし、これは正確ではありません。

したがって、要約すると:





環境インフラ



SOEnvの中には、SOEnvが見た目よりも複雑なエンティティになる興味深いものがあります。 これはSObjectizer Environment Infrastructure(または、簡単に言うとenv_infrastructure)です。 これが何であり、なぜかを説明するために、SObjectizerが完全に異なるタイプのタスクで使用されたときに遭遇した興味深い状態について話す必要があります。



SObjectizer-5が登場したとき、SOEnvはマルチスレッドを使用して仕事をしていました。 そのため、タイマーは個別のタイマースレッドとして実装されました。 SOEnvが協同組合の登録解除を完了し、協同組合に関連するすべてのリソースを解放する作業スレッドが別にありました。 また、デフォルトのディスパッチャは別の作業スレッドであり、デフォルトのエージェントに関連付けられたディスパッチャのリクエストが処理されます。



SObjectizerは複雑なマルチスレッドアプリケーションの実装を簡素化するように設計されているため、SObjectizer自体でのマルチスレッドの使用は、完全に通常の許容可能なソリューションと見なされました(現在考慮中です)。



しかし、時間が経つにつれて、SObjectizerは以前は考えもしなかったプロジェクトで使用されるようになり、マルチスレッドSOEnvが冗長で高価になりすぎる状況が現れ始めました。



たとえば、定期的に起動し、一部の情報の存在を確認し、この情報が表示されたときに処理し、結果をどこかに追加して再びスリープする小さなアプリケーションです。 すべての操作は、単一のワークフローで実行できます。 さらに、アプリケーション自体は軽量である必要があり、SOEnv内に追加のワークフローを作成するコストを回避したいと思います。



別の例:Asioを介してネットワークでアクティブに動作する別の小さなシングルスレッドアプリケーション。 しかし同時に、ロジックの一部はAsioではなく、SObjectizerエージェントで行う方が簡単です。 この場合、AsioとSObjectizerの両方を同じ作業コンテキストで動作させたいと思います。 さらに、機能の重複も避けたいと思います:Asioが使用され、Asioには独自のタイマーがあるため、SObjectizerがAsioタイマーを使用して遅延および定期的なメッセージを処理する場合でも、SOEnvで同じメカニズムを実行しても意味がありません。



このような特定の条件下でSObjectizerを使用できるようにするために、env_infrastructureの概念がSOEnvに登場しました。 C ++レベルでは、env_infrastructureはメソッドのセットを持つインターフェースです。 SOEnvが開始すると、このインターフェイスを実装するオブジェクトが作成され、その後、SOEnvはこのオブジェクトを使用してジョブを実行します。







SObjectizerには、env_infrastructureの既製の実装がいくつか含まれています。通常のマルチスレッド。 シングルスレッド、同時にスレッドセーフではありません。 シングルスレッド、この場合はスレッドセーフです。 さらに、 so_5_extraには、Asioに基づいた単一スレッドのenv_infrastructureがいくつかあります。1つはスレッドセーフで、 もう1つはそうではありません。 ユーザーは自分のenv_infrastructureを書くことができますが、これは簡単な作業ではありません。 SObjectizerの開発者である私たちは、env_infrastructureインターフェイスが変更されないことを保証できません。 深すぎると、これはSOEnvと統合されます。



エージェント、協力、ディスパッチャー、disp_binders。 また、event_queue



SObjectizerを使用する場合、開発者は基本的に次のエンティティを処理する必要があります。





このセクションでは、エージェントとディスパッチャについて説明し、次に、メールボックスについて説明します。



ディスパッチャーとevent_queue



エージェントとディスパッチャとの会話を開始します。 ディスパッチャの目的を理解すると、エージェント、エージェントの協力、およびdisp_bindersからVenegretを把握するのが簡単になります。



SObjectizerの実装における重要なポイントは、SObjectizer自体がエージェントにメッセージを配信することです。 エージェントはループで受信メソッドを呼び出す必要はなく、受信から返されたメッセージのタイプを分析する必要はありません。 代わりに、エージェントは関心のあるメッセージをサブスクライブし、目的のメッセージが表示されると、SObjectizer自体がこのメッセージに対してエージェントのハンドラーメソッドを呼び出します。



ただし、このスキームで最も重要な質問は次のとおりです。SObjectizerはハンドラーメソッドをどこで正確に呼び出しますか? つまり エージェントはどの作業スレッドのコンテキストで、それに宛てられたメッセージを処理しますか?



これは単なるディスパッチャです。これは、エージェントがメッセージを処理するための作業コンテキストを提供するSObjectizerの本質です。 大まかに言うと、ディスパッチャは1つ以上のワークスレッドを所有しており、これらのスレッドでエージェントのハンドラメソッドが呼び出されます。



SObjectizerには、最も原始的なもの(one_threadやthread_poolなど)から高度なもの(adv_thread_poolやprio_dedicated_threads :: one_per_prioなど)まで、8つのフルタイムディスパッチャーが含まれています。 開発者は、アプリケーションに必要な数のディスパッチャを作成できます。



たとえば、コンピューターに接続された複数のデバイスを照会し、受信した情報を何らかの方法で処理し、データベースに格納して、この情報を何らかの種類のMQブローカーを通じて外部に送信するアプリケーションを作成する必要があるとします。 同時に、デバイスとの対話は同期的であり、データ処理は非常に複雑でマルチレベルになります。



デバイスごとにone_threadディスパッチャーを1つ作成できます。 したがって、デバイスに対するすべてのアクションは別のスレッドで実行され、このスレッドを同期操作でブロックしても、アプリケーションの他の部分には影響しません。 また、データベースを操作するためにone_threadマネージャーを個別に割り当てることができます。 残りのタスクでは、単一のthread_poolディスパッチャーを作成できます。







したがって、開発者がツールとしてSObjectizerを選択する場合、開発者の主なタスクの1つは、開発者に必要なディスパッチャを作成し、エージェントを対応するディスパッチャにリンクすることです。



event_queue



そのため、SObjectizerでは、エージェントは、処理を待機しているメッセージがあるかどうかを個別に判断する必要はありません。 代わりに、エージェントがバインドされているディスパッチャは、エージェントが受信したメッセージのエージェントハンドラメソッドを呼び出します。



しかし、ここで疑問が生じます:ディスパッチャは、メッセージがエージェントに宛てられていることをどのようにして見つけるのでしょうか?



質問は決して怠idleではありません。 「古典的な」アクターモデルでは、各アクターはアクターに宛てられた独自のメッセージキューを持っています。 SObjectizer-5の最初のバージョンでは、同じパスをたどりました。各エージェントには独自のメッセージキューがありました。 メッセージがエージェントに送信されると、メッセージはこのキューに格納され、エージェントが接続されているディスパッチャーはこのメッセージの処理を要求されました。 エージェントにメッセージを送信するには、エージェント自体のメッセージキューとディスパッチャのリクエストキューの2つのキューを補充する必要があることが判明しました。



このスキームには肯定的な側面がありましたが、それらはすべて大きな欠点、つまり非効率性によって平準化されていました。 したがって、すぐにSObjectizer-5で、エージェント用の独自のメッセージキューを放棄しました。 これで、エージェント宛てのメッセージが配置されるキューはエージェントではなく、ディスパッチャーに属します。



ディスパッチャがエージェントがメッセージを処理する場所とタイミングを決定する場合、ロジックは単純です。ディスパッチャがエージェントのメッセージキューを所有できるようにします。 したがって、SObjectizerで次の図が表示されます。







エージェントとディスパッチャ間の接続要素はevent_queueです。これは、ディスパッチャの対応するディスパッチキューにエージェントメッセージを保存する特定のインターフェース持つオブジェクトです。



event_queueオブジェクトは、ディスパッチャーが所有しています。 event_queueの実装方法、event_queueオブジェクトの数、各event_queueが一意であるかどうか、複数のエージェントが共通のevent_queueオブジェクトで動作するかどうかなどを決定するのはディスパッチャです。



エージェントは、最初はディスパッチャーとの接続を持っていません。この接続は、エージェントがディスパッチャーにバインドされているときに表示されます。 エージェントがディスパッチャに接続された後、エージェントにはevent_queueへのリンクがあり、エージェント宛のメッセージがエージェントに送信されると、このメッセージはevent_queueに送信され、event_queueはすでにメッセージ処理のリクエストがディスパッチャの必要なキューにあることを確認する責任があります。



同時に、エージェントの人生にはエージェントがディスパッチャと接続していない、つまり エージェントには、そのevent_queueへの参照がありません。 最初のポイントは、エージェントの作成と、登録時のディスパッチャーへのバインディングとの間のギャップです。 2番目のポイントは、登録解除中に、エージェントが既にディスパッチャからアンタイドされているが、まだ破棄されていない期間です。 これらの瞬間にメッセージがエージェントに宛てられた場合、配信中にエージェントにevent_queueがないことが明らかになり、この場合、メッセージは単にスローされます。



エージェント、協力、disp_binders



SObjectizerでのエージェントの起動は4段階で行われます。



最初の段階で、プログラマーは空の協力関係を作成します(詳細は以下)。



第二段階では、プログラマーはエージェントのインスタンスを作成します。 SObjectizerのエージェントは通常のC ++クラスによって実装され、エージェントの作成はこのクラスの通常のインスタンス作成として実行されます。



第3段階では、プログラマーはエージェントを協力に追加する必要があります。 協力は、私たちが知る限り、SObjectizerのみにあるもう1つのユニークなものです。 協力とは、SOEnvに表示され、SOEnvから同時にかつトランザクション的に消えなければならないエージェントのグループです。 つまり、3人のエージェントが協力している場合、3人全員がSOEnvで作業を正常に開始する必要があります。そうしないと、いずれも作業を開始できません。 同様に、3つのエージェントすべてがSOEnvから同時に削除されるか、3つのエージェントすべてが引き続き連携して動作します。



ほとんどの場合、エージェントは1つずつではなく、相互接続されたグループによってアプリケーション内に作成されることが明らかになったときに、協力の必要性はSObjectizerの作業の開始時にほぼ即座に発生しました。 そして、開発者がグループの開始とロールバックの実装のための制御スキームを考え出すことを防ぐために、必要な3つのエージェントのうち2つが正常に起動し、3つ目のエージェントが起動せず、協同組合が発明されました。



したがって、3番目のステップで、プログラマーはエージェントとの協力関係を満たします。 協力が一杯になった後、4番目のステップである協力の登録が続きます。 コードでは、次のようになります。



 so_5::environment_t & env = ...; // SOEnv     . //  №1:  . auto coop = env.create_coop("demo"); //  №2:  ,      . auto a = std::make_unique<my_agent>(... /*   my_agent*/); //  №3:    . coop->add_agent(std::move(a)); ... //  №4:  . env.register_coop(std::move(coop));
      
      





しかし、通常、これはよりコンパクトな形式で行われます。
 so_5::environment_t & env = ...; // SOEnv     . env.introduce_coop("demo", [](so_5::coop_t & coop) { //1   . //     №2  №3. coop.make_agent<my_agent>(... /*   my_agent*/); ... }); //  №4  .
      
      







協力を登録するとき、開発者は作成および完了したSOEnv協力を転送します。 SOEnvは多くのアクションを実行します。連携名の一意性を確認し、連携エージェントに必要なリソースからディスパッチャを要求し、エージェントでso_define_agent()メソッドを呼び出し、エージェントをディスパッチャにバインドし、エージェントでso_evt_start()メソッドが呼び出されるように特別なメッセージを送信します。 当然、以前に実行されたアクションのロールバックにより、このリストの一部の操作が失敗した場合。



協力が登録されると、エージェントはすでにSObjectizer内(より正確には特定のSOEnv内)にあり、完全に機能します。



協力の登録の最も重要な部分の1つは、エージェントをディスパッチャにバインドすることです。 エージェントがevent_queueへの実際のリンクを持っているのはバインディングの後です。これにより、メッセージをエージェントに配信できます。



協力の登録が成功すると、何らかの画像が表示されます。





disp_binder



「ディスパッチャへのエージェントのバインド」を数回言及しましたが、このバインドがどのように実行されるかについてはまだ説明していません。 SObjectizerは、各エージェントがどの種類のディスパッチャにバインドされるべきかをどのように理解しますか?



そして、ここでdisp_binderと呼ばれる特別なオブジェクトが実行されます。 エージェントとの連携を登録するときに、エージェントをディスパッチャにバインドするだけです。 また、協力の登録解除中にエージェントをディスパッチャから解放するため。



SObjectizerは、すべてのdisp_bindersがサポートする必要があるインターフェイスを定義します 。 具体的なdisp_binder実装は、ディスパッチャの特定のタイプに依存します。 そして、各ディスパッチャは独自のdisp_binderを実装します。



エージェントをディスパッチャにバインドするには、開発者はdisp_binderを作成し、エージェントを連携に追加するときにこのdisp_binderを指定する必要があります。 実際、協力関係を満たすためのコードは次のようになります。



 auto & disp = ...; //   ,     . env.introduce_coop("demo", [&](so_5::coop_t & coop) { //    ,  disp_binder  . coop.make_agent_with_binder<my_agent>(disp->binder(), ... /*    my_agent */); ... });
      
      





重要な点:disp_binderを所有するのは協力であり、disp_binderが使用するエージェントは協力のみが知っています。 したがって、登録された協力の実際の状況は次のようになります。





メールボックス



少なくとも表面的に考慮するのが理にかなっているSObjectizerのもう1つの重要な要素は、メールボックス(またはSObjectizerの用語ではmbox)です。



メールボックスの存在は、SObjectizerを「クラシック」アクターモデルを実装する他のアクターフレームワークと区別します。 「クラシック」アクターモデルでは、メッセージは特定のアクターに宛てられます。 したがって、メッセージの送信者は受信者アクターへのリンクを知っている必要があります。



SObjectizerを使用すると、レッグはアクターのモデルから(だけではなく)成長するだけでなく、パブリッシュ/サブスクライブメカニズムからも成長します。 したがって、SObjectizerに最初から組み込まれている1:Nモードでメッセージを送信する操作があります。 したがって、SObjectizerでは、メッセージはエージェントに直接送信されるのではなく、mbox-sで送信されます。 mboxの背後には、受信者エージェントが1人いる場合があります。 または数人(または数十万人の受信者)。 またはまったくありません。



メッセージは受信者エージェントに直接送信されず、メールボックスに送信されるため、「クラシック」アクターモデルではなく、Publish-Subscribeの基礎となる別の概念を導入する必要がありました。mboxからのメッセージのサブスクライブです。SObjectizerでは、エージェントがmboxからメッセージを受信する場合、そのメッセージをサブスクライブする必要があります。サブスクリプションなし-メッセージはエージェントに届きません。サブスクリプションがあります-彼らは到達しています。





ネイティブmboxタイプ



SObjectizerには2種類のmboxがあります。最初のタイプは、マルチプロデューサー/マルチコンシューマー(MPMC)です。このタイプのmboxは、M:Nモードで相互作用を実装するために使用されます。2番目のタイプは、マルチプロデューサー/シングルコンシューマー(MPSC)です。このタイプのmboxは後で登場し、M:1モードでの効果的な対話用に設計されています。



当初、SObjectizer-5にはMPMC-mboxしかありませんでした。これは、M:N配信メカニズムで問題を解決するのに十分だからです。また、M:Nモードで相互作用が必要な場合、およびM:1モードで相互作用が必要な場合(この場合、単一の受信者が所有する別個のmboxが作成されます)。ただし、M:1モードでは、MPMC mboxのオーバーヘッドがMPSC mboxと比較して高すぎるため、SObjectizerでのM:1対話の場合のオーバーヘッドを削減するには、MPSC mboxが追加されました。

. MPSC-mbox- SObjectizer , . , , . MPSC-mbox- .

Multi-Producer/Multi-Consumer mbox-



MPMC-mboxは、メッセージをサブスクライブしたすべてのエージェントにメッセージを配信する役割を果たします。そのようなエージェントが単数であろうとなかろうと、そのようなエージェントはたくさんいるでしょう。そういったエージェントはまったくいないでしょう。したがって、MPMC-mboxは、各タイプのメッセージのサブスクライバーのリストを保存します。また、MPMC-mboxの一般的なスキームは次のように表すことができます。







ここで、Msg1、Msg2、...、MsgNは、エージェントがサブスクライブするメッセージのタイプです。



マルチプロデューサー/シングルコンシューマmbox



MPSC-mboxはMPMC-mboxよりもはるかに単純であるため、より効率的に動作します。MPSC-mboxでは、このMPSC-mboxが関連付けられているエージェントへのリンクのみが保存されます。







「指で」エージェントにメッセージを配信するためのメカニズム



SObjectizerのメッセージが受信者に配信される方法について簡単に説明すると、次の図が







表示されます。メッセージはmboxに送信されます。 Mboxは受信者を選択し(MPMC-mboxの場合、これらはすべてこのタイプのメッセージのサブスクライバーです。MPSC-mboxの場合、これはmboxの唯一の所有者です)、メッセージを受信者に送信します。



受信者は、event_queueへの実際のリンクがあるかどうかを確認します。その場合、メッセージはevent_queueに渡されます。 event_queueへの参照がない場合、メッセージは無視されます。



メッセージがevent_queueに渡された場合、event_queueは適切なディスパッチャーキューにメッセージを保存します。このキューの内容は、ディスパッチャの種類によって異なります。



ディスパッチャは、キューをレイクしているときにこのメッセージに到達すると、作業コンテキストでエージェントを呼び出します(大まかに言って、作業スレッドの1つのコンテキストで)。エージェントは、このメッセージのハンドラーメソッドを見つけて呼び出します(再度、ディスパッチャーによって提供されたコンテキストで呼び出しが行われることを強調します)。



実際、SObjectizerのメッセージ配信メカニズムの動作原理については、一般的に言えばそれだけです。詳細はやや複雑ですが、今日は詳細を調べません。



おわりに



この記事では、SObjectizerの主要なメカニズムと機能の、表面的ではあるが理解しやすい概要を作成しようとしました。この記事が、SObjectizerの仕組みを理解するのに役立つことを願っています。そして、SObjectizerの用途を理解する方が良いかもしれません。



しかし、何かを理解していないか、何かについてもっと知りたい場合は、コメントで質問してください。質問されたときに気に入っており、喜んでお答えします。同時に、質問をするすべての人に感謝します-SObjectizer自体とそのドキュメントの両方の改善と開発を私たちに強制します。



また、この機会を利用して、SObjectizerに精通していないすべての人を招待して、フレームワークに慣れてもらいたいと思います。 C ++ 11(最小要件はgcc-4.8またはVC ++ 12.0)で記述されており、Windows、Linux、FreeBSD、macOSで動作し、CrystaxNDKを使用するとAndroidで動作します。 BSD-3-CLAUSEライセンスに基づいて配布(無料)。githubまたはSourceForgeで取得できます。現在利用可能なドキュメントはこちらです。さらに、SObjectizerには多数のサンプルが含まれており、はい、すべて最新です:)



見て、突然好きになるでしょう。気に入らない場合はお知らせください。修正を試みます。フィードバックは今私たちにとって非常に重要なので、SObjectizerで自分に必要なものが見つからなかった場合は、それについて教えてください。おそらく、SO-5の将来のバージョンでこれを追加できるでしょう。



All Articles