12.31.2017

初學Actor Model - Akka.Net

此篇文章基本上是Akka.Net的基礎教學的筆記。

什麼是Actor?

Actor是一個系統中對參與者的模擬。它可能是一個實體,一個類別,可以做一些事情,及互相溝通。

Actors如何互相溝通?

Actors透過訊息互相溝通,任何POCO都可以是訊息,字串、整數、類別、或實作了一個介面的物件。

//this is a message!
public class SomeMessage
{
    public int SomeValue {get; set}
}

傳送訊息:

//send a string to an actor
someActorRef.Tell("this is a message too!");

Actor可以做什麼?

除了處理收到的訊息外,一個actor還可以:
* 建立另一個actor
* 傳送訊息給另一個actor
* 變更它自己的行為,並處理不同的訊息(相對於原先的行為的訊息)

什麼是一個ActorSystem

一個ActorSystem是對Akka.NET framework內部系統的一個參考,所有的actor都生存在其中,你也需要以它來建立你的actor。

安裝Akka.Net NuGet

Install-Package Akka

建立ActorSystem

var system = ActorSystem.Create("MyActorSystem");

建立一個Actor

var writerActor = system.ActorOf(
    Props.Create(
        () => new WriterActor()),
        "writerActor");

或是使用泛型的方式:

var writerActor = system.ActorOf(
    Props.Create<WriterActor>(), "writerActor");

PropsIActorRef’s

什麼是Props

Props是一個封裝了所有需要建立實體actor的資訊的設定類別。

如何建立Props

  1. 使用lambda語法:
Props props = Props.Create(() => new MyActor(..), "...");
  1. 使用泛型語法:
Props props = Props.Create<MyActor>();

什麼是IActorRef

IActorRef代表對一個actor的參考。目的是支援透過ActorSystem傳送訊息給一個actor。

子Actor,Actor階層及監管

enter image description here
建立最上層的actors:

// create the top level actors from above diagram
IActorRef a1 = MyActorSystem.ActorOf(Props.Create<BasicActor>(), "a1");
IActorRef a2 = MyActorSystem.ActorOf(Props.Create<BasicActor>(), "a2");

建立a2下的子actors:

// create the children of actor a2
// this is inside actor a2
IActorRef b1 = Context.ActorOf(Props.Create<BasicActor>(), "b1");
IActorRef b2 = Context.ActorOf(Props.Create<BasicActor>(), "b2");

Actor階層中如何監管?

每個actor會監管其子actor,且僅止於其子actor,如上面的階層圖,a2僅監管b1及b2。

何時需要監管?

發生錯誤時!
當一個子actor發生未處理的例外且要掛掉時,它會尋求它的父actor的幫忙。它會傳送一個Failure類別的訊息,並由父actor決定如何處理。

監管規則

當父actor收到來自其子actor的錯誤訊息,它可以決定以下列方式處理:
* 重啟子actor(預設行為)
* 停止子actor
* 上報錯誤(且停止自己的動作)

監管策略

有兩個內建的策略型態:
* 一對一:父actor發佈的命令僅針對發生錯誤的子actor
* 一對全部:父actor發佈的命令不僅只針對發生錯誤的子actor,而是其所有的子actor

重點是什麼?隔離!

如下圖所示:
Error Kernel
我們切分工作,把可能會發生錯誤的地方放在某一節點上,這讓潛在的錯誤被隔離,系統不會因為一個節點的錯誤而崩潰!

使用ActorSelection依位址尋找特定Actor

var selection = Context.ActorSelection("/path/to/actorName");
selection.Tell(message);

或使用ActorOf:

class FooActor : UntypedActor {}
Props props = Props.Create<FooActor>();

// the ActorPath for myFooActor is "/user/barBazActor"
// NOT "/user/myFooActor" or "/user/FooActor"
IActorRef myFooActor = MyActorSystem.ActorOf(props, "barBazActor");

// if you don't specify a name on creation as below, the system will
// auto generate a name for you, so the actor path will
// be something like "/user/$a"
IActorRef myFooActor = MyActorSystem.ActorOf(props);

也可參考:When Should I Use Actor Selection?

Actor的生命週期

lifecycle methods

使用ReceiveActor來更聰明的處理訊息

public class StringActor : ReceiveActor
{
    public StringActor()
    {
        Receive<string>(
            s => s.StartsWith("AkkaDotNet"), 
            s =>{ /* handle string */ });

        Receive<string>(
            s => s.StartsWith("AkkaDotNetSuccess"), 
            s =>{/* handle string*/});
    }
}

使用BecomeStackedUnbecomeStacked在執行時變更Actor的行為

public class UserActor : ReceiveActor {
    private readonly string _userId;
    private readonly string _chatRoomId;

    public UserActor(string userId, string chatRoomId) {
        _userId = userId;
        _chatRoomId = chatRoomId;

        // start with the Authenticating behavior
        Authenticating();
    }

    protected override void PreStart() {
        // start the authentication process for this user
        Context.ActorSelection("/user/authenticator/")
            .Tell(new AuthenticatePlease(_userId));
    }

    private void Authenticating() {
        Receive<AuthenticationSuccess>(auth => {
            Become(Authenticated); //switch behavior to Authenticated
        });
        Receive<AuthenticationFailure>(auth => {
            Become(Unauthenticated); //switch behavior to Unauthenticated
        });
        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // can't accept message yet - not auth'd
            });
        Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // can't send message yet - not auth'd
            });
    }

    private void Unauthenticated() {
        //switch to Authenticating
        Receive<RetryAuthentication>(retry => Become(Authenticating));
        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // have to reject message - auth failed
            });
        Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // have to reject message - auth failed
            });
    }

    private void Authenticated() {
        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // print message for user
            });
        Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // send message to chatroom
            });
    }
}

什麼是Switchable behavior?

在Actor Model中,一個Actor的核心屬性是它可以在訊息的處理過程中變更自己的行為。
這個特性讓Actor可以實作有限狀態機,或根據所收到的訊息變更處理的方式。

如何實作

  • 堆疊式

    • BecomeStackedenter image description here
    • UnbecomeStackedenter image description here
  • 直接變更

    • Become

Written with StackEdit.

12.16.2017

JackTimingApp

正在學Xamarin.Forms,順便用之前WPF的專案修改,原本想共用ViewModel,不過繪圖的部份改用SkiaSharp,加上原先並沒有把繪圖的部份分開,所以就重寫了XD。

初步成果如下圖:
Screen Shot1

程式的部份還是採用MVVM,另還用了XF範例中的EventToCommand方式,如下程式:

<skia:SKCanvasView x:Name="canvasView" Grid.Row="0">
            <skia:SKCanvasView.Behaviors>
                <jackTimingApp:EventToCommandBehavior Command="{Binding PaintSurfaceCommand}"
                EventName="PaintSurface" />
            </skia:SKCanvasView.Behaviors>
        </skia:SKCanvasView>

這樣子繪圖的部份就由ViewModel包了,當然這次把繪圖的動作分開了,如下所示:

PaintSurfaceCommand = new Command((arg) =>
{
    var args = arg as SKPaintSurfaceEventArgs;

    var info = args.Info;
    var surface = args.Surface;
    var canvas = surface.Canvas;

    if (_bitmap == null)
        _bitmap = new SKBitmap(info);

    if (_drawEngine == null)
        _drawEngine = new SkiaDrawEngine(_bitmap);    

    TimingDatas = TimingMapParser.Parse(TimingData);
    _bitmap = _drawEngine.Draw(TimingDatas);

    canvas.DrawBitmap(_bitmap, 0, 0);
    UpdateTimingDiagram();
});

而實際更新canvas的動作就交由XF內建的MessagingCenter來完成:

public MainPage(MainViewModel vm)
{
    InitializeComponent();

    BindingContext = vm;

    MessagingCenter.Subscribe<MessageToken>(this, "message", (item) =>
    {
        switch (item.TokenType)
        {
            case MessageTokenType.UpdateTimingDiagram:
                DrawTiming();
                break;
        }
    });
}

private void DrawTiming()
{
    canvasView.InvalidateSurface();
}

繪圖的部份還有滿多可以調整的,另外也沒有加上檔案的處理,但介面的部份感覺不太適合App的承現,有時間再調整了…

Written with StackEdit.