Mittwoch, 16. Dezember 2015

Akka.NET Adventskalender – Tür 16

Endlich Parallel

Gestern haben wir erfolgreich unsere Rechenaufgabe in kleinere Teile zerlegt, die wir allerdings nur einem Worker Aktor zur Bearbeitung gegeben haben. Weil jeder Aktor nacheinander seine Nachrichten abarbeitet, konnten wir leider noch keinen Erfolg verbuchen, was die Verbesserung der Laufzeit unserer Berechnung angeht.

Wir müssen uns also etwas einfallen lassen, um die Aufgabe tatsächlich zu parallelisieren. Nur so können wir die erwartete Beschleunigung erhalten. Auch nun ahnst Du sicher schon, was kommt: Neue Aufgabe – neuer Aktor. Richtig! Das Muster hinter dem wir heute her sind, trägt den Namen "Router". Ein Router enpfängt Nachrichten und verteilt diese an einen oder mehrere Aktoren nach einem in ihm definierten Regelwerk. Wenn wir also einen Router erzeugen, der mehrere Aktoren hat, unter denen er alle eintreffenden Rechen-Aufgaben sinnvoll aufteilt (z.B. nach Round Robin), dann müsste sich doch die ersehnte Beschleunigung erreichen lassen.

Also passen wir zunächst unsere Erzeugung von Worker Aktoren im Master so an, dass wir anstelle eines Worker Aktors den Router erzeugen. Dieser kümmert sich dann um den Rest, verhält sich aber aus Master Sicht genau wie ein Worker.

public Master()
{
    // removed sequential worker
    // worker = Context.ActorOf(Props.Create<Worker>());
    worker = Context.ActorOf(Props.Create<RoutingWorker>());
 
    Receive<CalculatePi>(c => DoCalculation(c));
    Receive<double>(s => AddToSum(s));
}

Jetzt müssen wir uns für den Router die passende Logik einfallen lassen. Problem dabei ist, dass unsere Worker ihre Ergebnisse via "Sender.Tell()" dem Master mitgeteilt haben. Wenn wir nun einen Router zwischen die beiden setzen, müssen wir im Router entweder genau die Struktur der Antworten kennen und diese dann unsererseits weiterleiten – oder es gibt noch einen Trick. Zum Glück ist Letzteres der Fall, da Konstrukte wie ein Router doch recht häufig notwendig werden. Ein Aktor bietet eine Forward() Methode, mit der man Nachrichten unter Beibehaltung des ursprünglichen Absenders weiter leiten kann. Das wenden wir nun an wenn wir unseren Router schreiben:

public class RoutingWorker : ReceiveActor
{
    const int NrWorkers = 4;
    
    private IActorRef[] workers;
    private int nextWorker;
    
    public RoutingWorker()
    {
        workers = new IActorRef[NrWorkers];
        for (var i = 0; i < NrWorkers; i++)
            workers[i] = Context.ActorOf(Props.Create<Worker>());
        nextWorker = 0;
        
        Receive<Worker.CalculateRange>(r => ForwardToWorker(r));
    }
    
    private void ForwardToWorker(Worker.CalculateRange message)
    {
        workers[nextWorker].Forward(message);
        nextWorker = (nextWorker + 1) % NrWorkers;
    }
}

Sieht wie üblich nicht allzu kompliziert aus. Wenn Du nun das Berechnungsprogramm startest, wirst Du abhängig von Deinem Rechner (genauer gesagt den Fähigkeiten Deiner CPU) und der Konstante NrWorkers mehr oder weniger große Unterschiede feststellen können. Am besten Du nimmst Dir ein paar Minuten und spielst ein wenig mit diesem Parameter. Bin gespannt wie hoch Deine Steigerung ausfällt...

Da das Muster "Routing" so häufige Verwendung findet, bietet Akka.NET dafür eine sehr einfache Lösung an. Ich hoffe Du verteufelst mich nun nicht dafür, dass wir zuerst den umständlichen Weg gegangen sind und unseren Router selbst geschrieben haben. Aber es gibt ja das goldene Gesetz der IT, dass jeder Entwickler während seiner Karriere einmal ein JavaScript-Framework, eine HTML Template Engine und einen Akka.NET Router geschrieben haben muss. Was den letzten Punkt angeht, den können wir hiermit gemeinsam abhaken. Ab morgen kennen wir einen besseren Weg.

Keine Kommentare:

Kommentar veröffentlichen