Follow my new blog

Freitag, 19. Februar 2010

Einfacher im Gleichschritt – Dienstaufrufe mit der CCR synchronisieren

In der dotnetpro 3/2010 gehen Tobias Richling et al. in einem Artikel der Frage nach, wie asynchrone WCF-Dienstaufrufe synchronisiert werden können. Sie überlegen also, wie ein Client, der Service S1 und Service S2 asynchron (!) aufruft, es so einrichten kann, dass er erst weiter macht, wenn beide (!) Services ein Resultat geliefert haben.

Bei synchronen Aufrufen stellt sich diese Frage nicht. Da geht es ohnehin erst weiter im Client, wenn beide Aufrufe zurückgekehrt sind.  Hier das Beispiel dazu aus dem Artikel:

image

Im Client geht es erst nach der letzten Zeile weiter, wenn sowohl GetArticleList() wie auch GetSpecialOfferArticle() synchron aufgerufen wurden.

Symptomkur

Was aber, wenn beide Aufrufe asynchron sind? Bei Silverlight ist das gar nicht anders möglich. Und bei anderen Client-Plattformen sollten Sie auch darüber nachdenken, denn asynchrone Aufrufe bringen Performancevorteile. Sie laufen ja parallel, egal ob der Client mit einem oder mehreren Prozessorkernen ausgestattet ist.

Im Artikel sieht ein asynchroner Aufruf mit WCF so aus:

image

Nicht ganz trivial, oder? Und davon gibt es zwei – die dann eben, wenn nacheinander ausgeführt, zum Problem führen: Der Client muss irgendwie dafür sorgen, dass er auf die Ergebnisse von proxy.GetArticleListAsync() und proxy.GetSpecialOfferArticleAsync() wartet, bevor er weiter macht.

Wie das gehen kann, dazu macht sich der Artikel Gedanken und stellt am Ende eine allgemeine Lösung vor. Mit der sieht das Warten dann so aus:

image

Man registriert einen Eventhandler (AllComplete), der gerufen wird, wenn schließlich beide Dienstaufrufe zurückgekehrt sind, registriert die Dienstaufrufe (AddServiceCall()), die getätigt werden sollen und startet sie schließlich (ExecuteAll()). Das alles macht möglich ein sog. ServiceCallSynchronizer.

Das sieht auf den ersten Blick doch ordentlich aus. Auf den zweiten schleicht sich bei mir aber zumindest kleines Unbehagen ein, weil die Dienstaufrufe nicht typsicher registriert werden. Die Methodennamen sind als Zeichenketten anzugeben. Ob es eine Methode gibt, stellt sich also erst zur Laufzeit heraus. Ebenso gibt es keine Argumentprüfung zur Compilezeit, weil ja AddServiceCall() nicht weißt, ob und welche Parameter eine Dienstmethode hat. Das ist beides nicht schön.

Wer mit WCF arbeiten muss, mag den Autoren aber dennoch danken. Sie lösen mit Liebe ein Problem, das ansonsten ein Projekt ins Stocken bringen kann. Immerhin umfasst der allgemeine Aufrufsynchronisierer einige Dutzend Zeilen, über die Sie sich nun nicht mehr den Kopf zerbrechen müssen:

image

Wer die Lösung im Detail sehen möchte, der lese den Artikel.

Das Übel an der Wurzel packen

Ich habe nichts gegen WCF – aber dieses Beispiel zeigt mir wieder, dass ich mit WCF nicht direkt arbeiten möchte in meinem Anwendungscode. Ich halte die im Artikel vorgestellte Lösung für eine Symptomkur. Das Symptom: WCF bietet keinen Synchronisationsmechanismus für asynchrone Aufrufe.

Und was ist das Grundproblem, wenn das nur ein Symptom ist? WCF ist das Grundproblem. Denn WCF ist ein Frameworkbolide, der immer noch in der synchronen Kommunikation verwurzelt ist. Asynchronizität geht zwar auch irgendwie, ist aber nicht der Fokus. Dabei ist Asynchronizität – zumindest aus meiner Sicht – eine unverbrüchliche Bedingung für die Kommunikation in verteilten Anwendungen.

Aber ich will hier nicht philosophisch werden ;-) Lieber zeige ich einfach, wie aus meiner Sicht das Übel an der Wurzel gepackt werden könnte. WCF will ich dabei gar nicht ersetzen. WCF soll nur für mich als Anwendungsprogrammierer unsichtbar werden.

Hier meine Lösung:

image

That´s it.

Nur eine Kleinigkeit fehlt darin, die ich aber nur ausgelagert habe, um das Abstraktionsniveau einheitlich zu halten. Doch das ist auch nur eine simple Erweiterungsmethode:

image

Sieht das übersichtlich aus? Ist das alles typsicher? War das “out of the box” möglich? Drei Mal Ja.

Übersichtlich, typsicher und “out of the box” ist das alles, weil ich eben nicht WCF benutze, sondern ein Kommunikationsframework, das konsequent auf Asynchronizität setzt. Das ist der Xcoordination Application Space (AppSpace), der als Open Source Komponente bei CodePlex liegt und die Verteilung von Anwendungen erlaubt, die mit Microsofts Concurrency Coordination Runtime (CCR) arbeiten.

Wie funktioniert nun mein Code? Dazu müssen Sie verstehen, wie Dienste im AppSpace aufgerufen werden. Das geschieht immer über sog. Ports. Das sind typisierte Nachrichtenkanäle, die die CCR zur Verfügung stellt.

Dienste halten Sie also nicht in Form von Methoden in der Hand, sondern als Ports. Sie können AppSpace-Dienste nur indirekt anstoßen vermittels von Nachrichten. AppSpace-Anwendungen haben also immer eine Event-Driven Architecture (EDA), denn Dienste registrieren Eventhandler auf ihren Ports, um an die Nachrichten ihrer Clients zu kommen.

Einen Dienst einzurichten, ist mit dem AppSpace sehr einfach. Hier die Dienstimplementation:

public class MyArticleService : PArticleService

{

    [XcoConcurrent]

    internal void ProcessGetArticleList(GetArticleList query)

    {

        …

    }

 

    [XcoConcurrent]

    internal void ProcessGetSpecialOfferArticle(GetSpecialOfferArticle query)

    {

        …

    }

}

Und hier der Code, mit dem der Dienst gehostet wird, so dass Clients ihn aufrufen können:

using(var server = new XcoAppSpace("wcf.port=12345"))

{

    server.RunWorker<PArticleService, MyArticleService>("TheArticleService");

    …

Das ist nicht sehr aufwändig, oder? Und ich muss gar nicht auf die Segnungen von WCF verzichten, wie der Konfigurationsstring des AppSpace zeigt. Nur sind mir die ganzen lästigen WCF-Details einerlei. Ich kann mich auf das Wesentliche konzentrieren: Geschäftslogik.

Und wo sind die ominösen CCR Ports? Die stecken im Kontrakt des Dienstes, denn natürlich hat der AppSpace auch sein ABC: Address, Binding, Contract. Die Adresse des Dienstes ist localhost:12345/TheArticleService. Das Binding ist Net.Tcp von WCF. Und der Contract sieht so aus:

[Serializable]

public class Article

{

    public string description;

}

 

public class PArticleService : PortSet<PArticleService.GetArticleList,

                                       PArticleService.GetSpecialOfferArticle>

{

    [Serializable]

    public class GetArticleList

    {

        public Port<Article[]> response;       

    }

 

    [Serializable]

    public class GetSpecialOfferArticle

    {

        public int forMonth;

        public int forYear;

 

        public Port<Article> response;

    }

}

Das sieht für Sie sicher ungewöhnlich aus. Aber im Grunde ist es nicht viel anders als bei WCF. Ein AppSpace-Dienst hat auch eine Schnittstelle. Die besteht aber nicht aus einem interface sondern aus einer Liste von CCR Ports (PArticleService). Auf jedem Port “lauscht” dann ein Eventhandler (s. Process…() Methoden in der Dienstimplementation).

Ports haben keine Signatur, sondern transportieren nur Nachrichten eines Typs. Deshalb müssen die üblichen Dienstmethodensignaturen in Nachrichtentypen übersetzt werden. Aus

Article GetSpecialOfferArticle(int forMonth, int forYear);

wird dann ein Port für GetSpecialOfferArticle-Nachrichten. Und jede Nachricht enthält die ursprünglichen Methodenparameter als Felder. Ein Resultat liefert die Dienstmethode schließlich via einem Antwort-Port (response).

Das ist anders als bei WCF und Webservices. Aber ich würde sagen, es ist nicht schwer zu verstehen. Vor allem ist es aber 1. symmetrisch und 2. weniger trugschlussbehaftet.

Die Symmetrie zwischen Client und Service ergibt sich daraus, dass die Kommunikation in beide Richtungen via Ports läuft. Dadurch werden z.B. Notifkationen ein Kinderspiel. Und Sie unterliegen weniger Trugschlüssen bzgl. der Kommunikation, weil Sie sofort sehen, dass sie verteilt/asynchron ist, dass sie damit mal länger dauern kann, dass der Dienst auch mal nicht antworten kann usw.

Der Client holt sich dann einen “Proxy” für einen entfernten AppSpace-Dienst mit ConnectWorker<T>(). Der bietet Ports entsprechend dem Kontrakt, an die der Client seine Nachrichten schickt.

Und jetzt der Trick mit der Synchronisation: Dafür muss ich keinen Aufwand treiben, weil die CCR einen Synchronisationsoperator bietet, ein Join. Mit ihm kann der Client ganz einfach darauf waren, dass Nachrichten in mehreren Ports angekommen sind. Im Beispiel sind das die Antwort-Ports, die der Client in die Nachrichten an den Dienst gesteckt hat.

Eine Anweisung (client.Join()) genügt daher, um den Client-Code, der mit den Ergebnissen beider Dienstaufrufe weiter arbeiten sollen, erst dann auszuführen, wenn auch beide Dienste geliefert haben. Egal, in welcher Reihenfolge sie fertig geworden sind.

Fazit

Ich kann mir nicht helfen, aber ich finde die AppSpace/CCR-Lösung verständlicher, kürzer und “ehrlicher”. Das liegt für mich daran, dass AppSpace/CCR für Asynchronizität gemacht sind. Wo WCF sich strecken muss, um seinem synchronen Erbe zu entwachsen, da sind AppSpace/CCR schon lange angekommen.

Asynchronen Code zu schreiben, ist ungewohnt. Aber mit ein wenig Übung werden Sie feststellen, dass schon in relativ einfachen verteilten Szenarien dadurch viele Vorteile entstehen. Gerade für den “kleinen Verteilungshunger zwischendurch”, d.h. dort, wo Sie bisher nicht an Verteilung zu denken gewagt haben, da bieten AppSpace/CCR Ihnen eine Plattform, die es Ihnen einfach macht, den Einstieg zu finden. Ob die Kommunikation dann “auf dem Draht” mit WCF (TCP, Named Pipes) oder MSMQ oder Jabber oder Named Pipes läuft, das ist Ihnen egal. Ihr Programmiermodell ist immer gleich. Probieren Sie es mal aus.

Kommentare:

Laurin Stoll hat gesagt…

Hallo Ralf,

Wiedermal sehr spannend. Sehr gerne würde ich den AppSpace schon lange breiter einsetzen. Leider plagt mich aber immer noch die CCR ein wenig. Habe ich das noch recht in Erinnerung, dass ich die eben nicht einfach verteilen darf? Das ist leider ein sehr grosses Handicap vom AppSpace...

Ralf Westphal - One Man Think Tank hat gesagt…

@Laurin: Was heißt "nicht einfach verteilen darf"? Die CCR darfst du - wie den AppSpace - so einfach verteilen, wie du ein Telerik Grid oder eine SmartInspect Logging Engine verteilen darfst.

Wenn du eine Lizenz hast, darfst du verteilen. Manche Lizenz kostet Geld. Ob sie das für die CCR für dich kostet, musst du klären. Das hängt von deiner Anwendung ab. Und davon, ob du eine MSDN Subscription hast.

Auch der AppSpace ist nicht mehr per se kostenlos. Open Source bedeutet ja nicht zwangsläufig umsonst. Das ist bei PostSharp so, bei db4o und bei vielen anderen auch - und nun ebenfalls beim AppSpace.

Es gibt eine kommerzielle Lizenz für den AppSpace. Wieviel die allerdings kostet, weiß ich grad gar nicht. Upps :-| Wird aber nicht die Welt sein.

In jedem Fall sollten selbst einmalig 100 oder 200 EUR dem Einsatz nicht im Wege stehen in einer kommerziellen Software, die du entwickelst. Die ist ja auch nicht umsonst, oder? Warum sollte dann der Einsatz der Entwickler des AppSpace dauerhaft umsonst sein?

Die Evaluation des AppSpace ist in jedem Fall unbegrenzt kostenlos. Wenn du dann herausgefunden hast, dass dir der AppSpace auch nur 1% deiner Entwicklungszeit spart, dann kannst du ja mal ausrechnen, wieviel das z.B. in einem Jahr an Geld ist. Sollten diese 1% nicht mehr als 200 EUR wert sein, dann wäre das natürlich sehr misslich für dich. Dann solltest du dir einen anderen Job suchen ;-)

-Ralf

Anonym hat gesagt…

Hi Ralf,

Interessanter Artikel. Meines Wissens nach ist das CCR/DSS Framework aber gerade fuer Silverlight (welches Du am Anfang des Artikels erwaehnst) nicht verfuebar?

Ralf Westphal - One Man Think Tank hat gesagt…

@Anonym: Tatsächlich ist die CCR offiziell nicht für Silverlight verfügbar. Es soll Microsoft intern eine Version geben - aber veröffentlicht ist sie nicht.

Bei meiner Antwort ging es jedoch nicht um Silverlight. Ich habe auch nicht in Abrede gestellt, dass die Lösung in der dnp einen Nutzen darstellt, wenn man denn mit WCF entwickeln muss.

Was ich zeigen wollte, ist vielmehr, dass es Alternativen zu WCF gibt. Und dass das WCF Problem hausgemacht ist. Synchronisation von async Aufrufen ist nicht per se schwierig, sondern nur dann, wenn man an soetwas bei einem API nicht von vornherein denkt.

Wer also anders kann, der sollte zweimal überlegen, ob er WCF direkt einsetzen will.

-Ralf

Stefan hat gesagt…

Hallo Ralf,

Ich nehme trotzdem an, dass WCF in Zukunft zunehmend eingesetzt werden wird, einfach weil es ein Standard von Microsoft ist.

Ich habe deshalb eine C# Library (AsyncWcfLib) ins Netz gestellt, welche Asynchronizität mit WCF vereinfacht und den Konfigurationsaufwand reduziert.

Wichtig scheint mir auch die Vor- und Rückwärtskompatiblität für Meldungen, die ich in WCF so zum ersten mal von einem grossen SW Hersteller realisiert sehe.

/Stefan

Ralf Westphal - One Man Think Tank hat gesagt…

@Stefan: Deine AsyncWcfLib find ich interessant. Darüber solltest du einen Bericht in der dotnetpro schreiben. Schicke mir eine Email und ich setze dich mit dem Chefred in Verbindung.

Dass WCF eine große Bedeutung hat und behalten wird, steht für mich auch außer Zweifel. Aber TCP Sockets und der Win32 API haben auch immer noch eine große Bedeutung. Nur wir sehen sie nicht mehr. Wir programmieren keine Sockets und keine Win32 API Calls mehr. (Jedenfalls allermeistens.)

Und genauso will ich WCF nicht sehen. Es soll seinen Dienst tun und alles mögliche ermöglichen. Wunderbar. Der API, den ich in meiner Anwendung sehe, soll das aber alles verbergen. Er soll auf einen höheren Niveau liegen. So wie der CCR Space bzw. der Application Space. Oder auch wie deine Library, die allerdings noch (ganz natürlich) WCF recht verhaftet ist.

-Ralf

Der Tobi hat gesagt…

Hallo Ralf,

vielen Dank für den interessanten Artikel. Die Lösung mit CCR sieht schon sehr schlank und elegant aus.

Mir geht es jedoch so wie vielleicht vielen anderen auch wie Stefan es angesprochen hat: beruflich bin ich auch WCF festgelegt.

Eine Sache wollte ich noch im Bezug auf deine Besprechung unseres Artikels richtig stellen: der vorgestellte Synchronisationsansatz kann auch mit einem Delegaten aufgerufen werden. Dadurch habe ich dann alle Parameter als auch den Methodennamen typsicher definiert. Nur die Parameterwerte werden noch als Object[] übergeben.

Ein Beispiel dazu habe ich hier gepostet.

Viele Grüße,

Tobias.