ActorLite:軽量レベルのActorモデル実装(下)

10701 ワード

前回の記事では,簡単なActorモデルを実現した.Actorを構築する場合は、単にActorタイプを継承し、Receiveメソッドを実装するだけでよい.前回の記事の最後に,このActorモデルの使用をC#を用いて実証した.でも今F#を試してみましょう.
C#Actorモデルを使用した欠陥
Erlangでは、各メッセージは、パターンマッチングを使用して「構造」または「フォーマット」を制限し、異なる意味を表す.C#タイプシステムの抽象的な能力はErlangよりはるかに優れているが、Erlangの「ダイナミック」により、開発者はプログラムの中で任意のタイプを任意に送信し、受信することができ、この「自由」はErlangに柔軟性をもたらした.我々のActorモデルでは、各Actorオブジェクトには特定のメッセージフォーマットが必要であり、このメッセージフォーマットは「Actorのすべての職責を表現する」という重任を担っていますが、1つのActorの職責は任意のデータを組み合わせたものである可能性があります.例えば、最も簡単な「チャット」プログラムで、そのActorは「人」を表しており、Erlangで実現するとこのように書かれる可能性があります.
loop() ->
    receive
        %         ,        
        {start, Person} ->
            Person ! {self(), {greeting, "  ")},
            loop();

        %         ,          
        {Person, {greeting, Message}} ->
            Person ! {self(), {say, "..."}},
            loop();

        %       ,    
        {Person, {say, Message}} ->
            Person ! {self(), {bye, "..."}},
            loop();

        ...
    end.

異なるメタグループ(tuple)は異なる原子(atom)と協力してメッセージの「意味」を表しますが、C#を使用してこれらの「コマンド」をどのように表現しますか?次の操作を行います.
  • object[]をメッセージタイプとして使用し、その要素を確認します.
  • はobjectをメッセージタイプとして使用し、メッセージの具体的なタイプを判断する.
  • は、列挙または文字列を使用して「コマンド」を表し、パラメータのセットに合わせます.

  • 1つ目のやり方は面倒です.2つ目は「定義してから使う」ことも容易ではありません.3つ目の方法は、落ち着いて言えば、「配布クラスライブラリ」のサポートがあれば理想的です.この文章のF#よりも理想的かもしれません.趙さんはこの機能を実現しようと努力しています.C#のこの特性が影響するからです.NETプラットフォームの下のすべてのActorモデル(最初の記事で述べたCCRまたはRetlang)の使用.
    現在、F#がこの問題を少し緩和できるかどうかを見てみましょう.
    F#でのActorモデルの使用
    Erlangには厳密なタイプのシステムはなく,その「メッセージタイプ」は完全に動的であるため,非常に柔軟である.では、F#にはどんな「法宝」がC#で出会った気まずい思いを解決することができますか?この問題では、F#にはC#をリードする3つのキーがあります.
  • フレキシブルタイプシステム
  • 強力なモードマッチング
  • 自由な文法
  • F#も強いタイプのコンパイル言語ですが(これはC#と一致します)、F#のタイプシステムはC#よりも柔軟です.たとえば、「チャット」の例では、次のタイプを「メッセージ」タイプとして記述できます.
    type Message = string
    type ChatMsg = 
        | Start of Person
        | Greeting of Person * Message
        | Say of Person * Message
        | Bye of Person * Message

    この定義では、F#タイプシステムの3つの特徴が使用されています.
  • 型別名:type Message=string.既存のタイプに別名を定義すると、より良い意味が得られます.C#がusingを使用して別名を定義するのとは異なり、「ソースコード」レベルの別名だけでなく、F#の別名をグローバルに定義できます.
  • Discriminated Unions:type ChatMsg=....Discriminated Unionsは、1つのタイプに複数のdiscriminatorを指定できます.各discriminatorは、1つの名前と別の特定のタイプで表されます.異なるdiscriminatorの具体的なタイプは異なることができる.
  • ユニット(Tuple):Person*Messageです.F#では、既存のタイプを順番に任意に組み合わせて新しいタイプを得ることができ、このタイプを「メタグループ」と呼ぶ.

  • Actorモデルでは、F#の3つの特別な特性を組み合わせて、メッセージの具体的なタイプを定義します.使用する場合、「モードマッチング」を使用して、CharMsgの異なるdiscriminatorを異なる「メッセージ」に処理することができます.具体的なActorタイプPersonは、次の定義を使用できます.
    and Person(name: string) = 
        inherit ChatMsg Actor()
        
        let GetRandom = 
            let r = new Random(DateTime.Now.Millisecond)
            fun() -> r.NextDouble()
    
        member self.Name = name
        
        override self.Receive(message) =
            match (message) with
    

    Personクラスのコンストラクション関数は、nameをパラメータとして受け入れ、Nameプロパティに配置します.内部にシステムを構築するGetRandom関数も定義した.Randomオブジェクトは、NextDoubleメソッドの値を返すたびに返されます(GetRandomメソッドを何回呼び出しても、GetRandomメソッドを定義するときに作成されたため、同じRandomオブジェクトが常に使用されていることに注意してください).一方、overrideのReceiveメソッドでは、「モードマッチング」を使用してmessageオブジェクトを処理します.
            //         
            | Start(p) -> 
                Console.WriteLine("   {0} {1}   ", self.Name, p.Name)
                Greeting(self, "Hi,    ?") |> p.Post

    上記の最後の行に注意してください.もともとはp.Post(...)の呼び出し方式を使用していましたが、今は「|>」の記号を使用しています.F#では、x|>fはf(x)を表し、f(g(h(x))のような冗長な呼び出し方式を明確な「メッセージ送信」形式に変えることができることを意味する.x|>h|>g|>fである.「メッセージ送信」もちょうど私たちに必要な「感覚」です.したがって、次のコードでもこのような方法を使用します.
            //    
            | Greeting(p, msg) ->
                Console.WriteLine("{0} {1}   :{2}", p.Name, self.Name, msg)
                if (GetRandom() < 0.8) then
                    Say(self, " ,  。") |> p.Post
                else
                    Bye(self, "  ,bye!") |> p.Post
            //     
            | Say(p, msg) ->
                Console.WriteLine("{0} {1}  :{2}", p.Name, self.Name, msg)
                if (GetRandom() < 0.8) then
                    Say(self, "   。") |> p.Post
                else
                    Bye(self, "    ,bye!") |> p.Post
            //   
            | Bye(p, msg) ->
                Console.WriteLine("{0} {1}  :{2}", p.Name, self.Name, msg)
    

    これでPersonタイプ定義が完了します.Personオブジェクトを3つ構築し、自由にチャットさせます.
    let startChat() =
        let p1 = new Person("Tom")
        let p2 = new Person("Jerry")
        let p3 = new Person("  ")
        Start(p2) |> p1.Post
        Start(p3) |> p2.Post
    
    startChat()
    

    結果は次のとおりです(内容はランダムな結果によって変わります).
       Tom Jerry   
       Jerry      
    Jerry      :Hi,    ?
    Tom Jerry   :Hi,    ?
    Jerry Tom  : ,  。
       Jerry  : ,  。
    Jerry     :   。
    Tom Jerry  :   。
    Jerry Tom  :   。
       Jerry  :   。
    Jerry     :   。
    Tom Jerry  :    ,bye!
       Jerry  :   。
    Jerry     :    ,bye!

    Actorモデルを使用したネットワークデータのキャプチャ
    もう一つの「現実」の例を見てみましょう.複数のActorが協力する必要があります.まず、「キャプチャ」データ用のActorを定義します.その唯一の役割は、メッセージを受信し、キャプチャ結果を返すことです.
    type Crawler() =
        inherit ((obj Actor) * string) Actor()
    
        override self.Receive(message) =
            let (monitor, url) = message
            let content = (new WebClient()).DownloadString(url)
            (url, content) |> monitor.Post
    

    「単品」方式でモニタオブジェクトを直接定義するには、次の手順に従います.
    let monitor =
        { new obj Actor() with
            override self.Receive(message) =
                match message with
                // crawling
                | :? string as url -> (self, url) |> (new Crawler()).Post
    
                // get crawled result
                | :? (string * string) as p ->
                    let (url, content) = p
                    Console.WriteLine("{0} => {1}", url, content.Length)
    
                // unrecognized message
                | _ -> failwith "Unrecognized message" }
    

    「キャプチャ」メッセージを受信するたびにmonitorはCrawlerオブジェクトを作成しurlを送信し、返信メッセージを待機します.使用する場合は、オブジェクトをmonitorに1つずつ「送信」すればよい.
    let urls = [
        "http://www.live.com";
        "http://www.baidu.com";
        "http://www.google.com";
        "http://www.cnblogs.com";
        "http://www.microsoft.com"]
    
    List.iter monitor.Post urls

    実行結果は次のとおりです.
    http://www.live.com => 18035
    http://www.google.com => 6942
    http://www.cnblogs.com => 62688
    http://www.microsoft.com => 1020
    http://www.baidu.com => 3402

    パフォーマンス分析
    最後に,このActorモデルの性能をもう少し簡単に解析した.
    「ロック」の観点から、このActorモデルの唯一のロックがメッセージキューへのアクセスである場合、これは基本的に唯一のボトルネックです.これをlock-freeのキューに置き換えると、Actorモデル全体が完全なlock-free実装であり、その「スケジューリング」性能は良好と言える.
    しかし、別の角度から言えば、このActorモデルのスケジューリングは非常に頻繁で、毎回1つのメッセージしか実行されません.1つのメッセージを実行するのに50ミリ秒しかかからないのに、1回のスケジューリングで100ミリ秒が必要な場合、このパフォーマンスのボトルネックは「スケジューリング」に落ちます.そのため、Actorモデルの性能をさらに向上する必要がある場合は、Dispatcher.Executeメソッドでは、例えば、1つのメッセージを実行するたびにnメッセージを実行するか、1時間以上のしきい値を超えて次のスケジューリングを行うように変更します.スケジューリングを減らすことも、Actorモデルのパフォーマンスを向上させる鍵の一つです.
    また、感じたら.NETが持つスレッドプールは性能が高くない、あるいはプログラムの他の部分の影響を受けるので、独立したスレッドプールで置き換えることもできます.
    もちろん、どんな性能の最適化も感覚だけで手をつけることはできません.すべてはデータで話さなければなりません.そのため、最適化するときは必ず適切なProfileメカニズムを確立し、一歩一歩の最適化が有効であることを保証しなければなりません.
     
    ソースコードおよびサンプルのダウンロード:http://code.msdn.microsoft.com/ActorLite