Follow my new blog

Posts mit dem Label Multithreading werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Multithreading werden angezeigt. Alle Posts anzeigen

Mittwoch, 10. Juni 2009

Asynchronizität und Verteilung üben - Szenarien für verteilte Anwendungen

Wer was Neues lernen will, tut das am besten zunächst mit Übungen. Chirurgen lernen neue Techniken erst an toten und/oder nicht menschlichen Lebewesen, Piloten lernen im Simulator. Und ich will den neuen Application Space ausprobieren oder allgemeiner asynchrone und verteilte Architekturen üben. Was sind aber Übungsaufgaben, an denen ich mich versuchen kann? Einen asynchronen und verteilten Service aufzusetzen ist ja trivial. Von dem Service dann auch noch Notifikationen zu bekommen oder Pub/Sub einzurichten, das ist auch trivial. Jeweils für sich genommen sind diese Dinge einfach - aber wie füge ich diese Bausteine zu etwas Größerem, Realistisch(er)em zusammen? Erst in einem umfassenderen Szenarion, das nicht von der Technik ausgeht, sondern von "Kundenanforderungen" kann ich auch feststellen, was einer Technologie wie dem Application Space noch fehlen mag (oder wo sie besonders geeignet ist).

Hier möchte ich nun einige Szenarien zusammentragen, die mir als Übungen für Verteilung und Asynchronizität erscheinen. Sie sind mehr oder weniger komplex, aber immer irgendwie "zum Anfassen". Jedes bietet für die zu übende oder evaluierende Technologie eine andere Herausforderung. Ich werde sie mit dem Application Space implementieren, wer mag, kann aber natürlich WCF pur oder mit Azure oder Jabber pur oder MassTransit oder NServiceBus oder Rhino Service Bus oder MSMQ pur oder TCP Sockets pur oder noch ganz andere Technologien damit ausprobieren. Ganz im Sinne der School of .NET Diskussion sehe ich diese Szenarien auch als Chancen für ganzheitliches Lernen. Clean Code Development, Komponentenorientierung, .NET Framework Grundlagen, TDD... all das und mehr kann man auch einfließen lassen.

Szenario 1: Stammdatenverwaltung

Aller Anfang sollte einfach und typisch sein. Deshalb ist mein erstes Szenario eines, mit dem viele Entwickler immer wieder konfrontiert werden: die Stammdatenverwaltung oder "forms over data". Ein Anwender verwaltet mit seinem Client Daten in einer Datenbank mit den üblichen CRUD-Funktionen: Create, Read, Update, Delete. Zusätzlich kann er einen serverseitigen Datenimport anstoßen.

Ob sich für dieses Szenario eine Verteilung überhaupt lohnt, sei einmal dahingestellt. Allemal, wenn aus anderen Gründen eine Anwendung verteilt werden soll, muss auch die Stammdatenverwaltung auf eine solche Architektur abgebildet werden.

Um das Datenmodell einfach zu halten, reicht es aus, wenn das Szenario sich nur um Personen mit ihrer Adresse dreht.

Datenmodell:

  • Person(Nachname, Vorname, Straße, PLZ, Ort, Land, Tel, Soundex)

Featureliste:

  • Der Anwender kann nach Personen suchen; der Server liefert eine Liste von passenden Personen zurück.
  • Der Anwender kann eine gefundene Person bearbeiten und speichern.
  • Der Anwender kann eine neue Person anlegen.
  • Der Anwender kann eine Person löschen.
  • Der Anwender kann gefundene Adressen in eine CSV-Datei exportieren. Der Export kann clientseitig erfolgen.
  • Der Anwender kann den Import von Personen aus einer CSV-Datei veranlassen. Dazu muss er dem Server mitteilen, in welcher Datei die Daten liegen. Der Server importiert, meldet zwischendurch den Fortschritt und liefert am Ende ein Importresultat.
  • Dublettenprüfung: Jede Person soll nur einmal in der Datenbank stehen. Mehrere Sätze mit denselben Daten sind zu vermeiden, um bei Mailings nicht mehrere Briefe an dieselbe Person zu senden. Um den Vergleich von Personen zu vereinfachen, können sie mit einem Soundex-Wert ausgestattet werden. Wannimmer eine Person gespeichert werden soll (nach Bearbeitung, nach Neuanlage, beim Import) und schon mit einem anderen Datensatz in der Datenbank vertreten ist, wird die Operation verweigert und der Anwender informiert. Die Dublettenprüfung kann in Schritten implementiert werden:
    • Dubletten bei Neuanlage prüfen
    • Dubletten beim Import prüfen
    • Dubletten nach Bearbeitung prüfen

Klingt doch einfach, oder? Hat aber natürlich seine Tücken, denn es gilt ja, diese Funktionalität asynchron und verteilt zu realisieren. Wie kommunizieren Client und Server im Sinne einer solchen Stammdatenverwaltung miteinander?

image

Herausforderungen:

  • Wie werden asynchrone Operationen wie Speichern oder Import im UI repräsentiert?
  • Wie meldet der Server den Fortschritt beim Im/Export an den Client (Notifikationen)?

Szenario 2: Referentenfeedback (Heckle Service)

Christian Weyer hat ein schon älteres Szenario in seinem dotnetpro-Artikel "Schnuppern an Azure" (5/2009)mit den aktuellen Technologien neu implementiert. In der dotnetpro 7/2009 greife ich das auf und realisiere es mit dem Application Space.

Die Idee ist einfach: Zuschauer eines Vortrags auf einer Konferenz sollen dem Referenten live Feedback geben können. Sie sollen sozusagen elektronisch zwischenrufen können (engl. to heckle). Dazu hat jeder Teilnehmer einen Client, mit dem er kurze Textnachrichten an den Referenten senden kann, der sie in einem eigenen Frontend auflaufen sieht.

Datenmodell:

  • Nachricht(Absendername, Nachrichtentext, Eingangszeitstempel) - Jede Nachricht gehört natürlich zu einem Referenten. Ob das allerdings in der Nachricht vermerkt werden muss, soll hier nicht festgelegt werden.

Featureliste:

  • Teilnehmer senden Zwischenrufe an den Referenten.
  • Teilnehmer sehen sich die Liste der letzten n Zwischenrufe an.
  • Der Referent bekommt jeden Zwischenruf automatisch angezeigt.
  • Falls der Referent sein Frontend - aus welchen Gründen auch immer - neu startet, bekommt er die Liste aller bisher eingegangenen Zwischenrufe angezeigt.
  • Der Referent identifiziert sich irgendwie, so dass die Teilnehmer ihm und keinem anderen ihre Zwischenrufe senden. Die Teilnehmer müssen einen Referenten also beim Zwischenrufen adressieren. Potenziell kann die Heckle-Anwendung ja gleichzeitig in vielen Vorträgen zum Einsatz kommen.
  • Der Veranstalter der Vorträge kann die Zwischenruflisten aller Referenten jederzeit einsehen.

image

Herausforderungen:

  • Wie nehmen Teilnehmer mit dem Referenten Kontakt auf? Direkt, indem sie seinen Rechner adressieren oder indirekt via eines Discovery-Servers?
  • Wo werden die Nachrichten vorgehalten, damit vor allem Teilnehmer und Veranstalter sich jederzeit einen Überblick verschaffen können?
  • Wie wird insb. der Referent automatisch über neue Nachrichten informiert?

Szenario 3: Tic Tac Toe

Es ist zwar kein typisches Geschäftsanwendungsszenario, aber es macht Spaß: ein Spiel realisieren. Bei Tic Tac Toe (TTT) sind die Regeln simpel, so dass man sich auf die verteilte Implementation konzentrieren kann.

Zwei Spieler spielen gegeneinander auf einem TTT-Brett. Jeder sitzt an seinem PC und sieht den gemeinsamen Spielstand.

Datenmodell:

  • Spielfeld mit 3x3 Spielfeldern in den Zuständen O, X und leer. Zusätzlich sollte das Spielfeld noch einen Spielzustand haben wie Spiel begonnen, Spiel beendet, Gewinner ist Spieler 1, Gewinner ist Spieler 2.

Featureliste:

  • Ein Spieler bietet sich zum Spiel an.
  • Ein Spieler nimmt Kontakt mit einem anderen auf und sie beginnen eine Partie.
  • Spieler machen Züge.
  • Ob und welcher Spieler gewinnt, wird automatisch festgestellt.
  • Ein Spieler beendet eine Partie vorzeitig.

Dies ist während des Spiels natürlich ein Peer-to-Peer-Szenario. Ob intern die Rollen aber auch gleich sind oder nicht vielleicht doch ein Spieler ein Partienserver ist, hängt von der Implementation ab.

image

Herausforderungen:

  • Wie nehmen die Spieler Kontakt miteinander auf?
  • Wo wird der Partienzustand gehalten?
  • Wie erfahren die Spieler über den nächsten Zug?
  • Wie wird den Spielern das Spielende mitgeteilt?

Szenario 4: Starbucks

Gregor Hohpe hat in einem Blogbeitrag deutlich gemacht, wie wenig praktikabel die bisher so beliebten 2-Phase-Commit-Transaktionen in der realen Welt, d.h. in asynchronen (und verteilten) Szenarien sind. MassTransit und Rhino Service Bus haben das aufgenommen und versucht, mit ihren Mitteln das Szenario abzubilden. Es ist einfach eine schöne Fingerübung für jeden, der in die verteilte und asynchrone Programmierung einsteigen will.

Bei Starbucks kommen Kunde, Kassierer und Barista zusammen. Der Kunde bestellt ein Getränk, der Kassierer nennt den Preis und nimmt das Geld entgegen. Währenddessen bereitet der Barista schon das Getränk zu und serviert es, wenn die Zahlung geklappt hat.

Datenmodell:

  • Bestellung(Getränkeart, Bechergröße, Menge)
  • Zahlungsaufforderung(Gesamtpreis einer Bestellung)
  • Bezahlung(Betrag, Zahlmittel) - Zahlmittel könnten Barzahlung oder Kreditkarte sein

Featureliste:

  • Kunde bestellt ein Getränk beim Kassierer. Variation: Kunde bestellt mehrere und verschiedene Getränke beim Kassierer.
  • Kassierer nennt den Gesamtpreis
  • Kunden bezahlt
  • Kassierer nimmt Bezahlung entgegen und prüft den Betrag. Wenn ok, dann schließt er die Bestellung ab.
  • Barista bereitet bestellte Getränke vor.
  • Wenn Bezahlung abgeschlossen, stellt der Barista die Getränke zur Abholung bereit.

Der Kunde kann hier als interaktiver Client realisiert werden. Kassierer und Barista hingegen sind automatische Dienste. Um die reale Welt nachzustellen, können ihre Funktionen über Pausen (Thread.Sleep()) eine wahrnehmbare Dauer bekommen.

image

Herausforderungen:

  • Wie nehmen die Beteiligten Kontakt miteinander auf?
  • Wie wird der Dialog zwischen Kunde und Kassierer geführt?
  • Wie erfährt der Kunde über das fertiggestellte Getränk?
  • Was passiert mit einem schon zubereiteten Getränk, wenn die Zahlung nicht erfolgreich ist?

Szenario 5: Arbeitsteilung

Das MSDN Magazine Juni/2009 beschreibt in "A Peer-To-Peer Work Processing App With WCF" ein Szenario, dass sich auch zur Übung zu realisieren lohnt. Mehrere sog. Worker stehen da bereit, um Aufträge von sog. Usern anzunehmen. Der Artikel nutzt zur Arbeitsverteilung das P2P-Protokoll von WCF - aber man kann es auch anders machen.

Datenmodell:

  • Arbeitsauftrag(Id, Dauer)

Featureliste:

  • User vergeben Aufträge in einen Pool von Workern hinein.
  • Worker übernehmen einen oder mehrere Arbeitsaufträge.
  • Worker können dem Pool beitreten oder ihn verlassen.
  • Ob ein Worker Aufträge annimmt hängt von seiner Last ab. Die gesamte Auftragslast soll natürlich möglichst gleichmäßig auf die Worker verteilt werden.
  • Wie die Arbeit "so läuft" können sog. Spectators beobachten; sie dienen der Instrumentierung des Systems.

In dieser Featureliste fehlen einige Aspekte, die der Artikel beschreibt, z.B. "Work Item Backup" oder "Work Sharing". Ich habe sie nicht aufgenommen, weil sie mir abhängig scheinen vom gewählten Lösungsweg (hier: P2P-Kommunikation). Meine Szenarien sollen aber keine Lösungswege oder Technologien nahelegen (z.B. Einsatz eines Busses oder Sagas oder P2P-Kommunikation).

Die schlanken Aufträge habe ich ebenfalls deshalb übernommen. Es geht ja nicht um eine konkrete Problemdomäne. So müssen die Aufträge nur eine Dauer haben, um Lastverteilung beobachten zu können.

image

Herausforderungen:

  • Wie wird die Auftragslast möglichst gleichmäßig (oder "gerecht") auf die Worker verteilt?
  • Wie werden weitere Worker möglichst unmittelbar in die Auftragsbearbeitung einbezogen? Oder allgemeiner: Wie kann der Worker-Pool "atmen"?
  • Wie werden User über Auftragsergebnisse (oder gar Fortschritte) informiert?
  • Wie kann ein Spectator die Arbeit(sverteilung) beobachten?
  • Müssen Aufträge nach Vergabe noch "gesichert" werden können, falls Worker noch nicht zu ihrer Verarbeitung gekommen sind?
  • Wie können die Worker möglichst ortsunabhängig verfügbar gemacht werden?

Donnerstag, 26. Juni 2008

Bound in Space - Skizze der Kommunikation in einem Application Space

Komponenten verteilen, Komponenten parallel arbeiten lassen und überhaupt Software aus Komponenten aufbauen sollte viel, viel einfacher werden. Denn nur dadurch würden sich diese Konzepte breiter durchsetzen. Und nur dadurch würde Software in größerer Menge performanter, skalierbarer, testbarer, evolvierbarer. Dafür habe ich in meinem gestrigen Posting eine Lanze gebrochen.

Heute möchte ich zu dieser Vision eines Application Space (AppSpace) ein Bild zeichnen. Ich stelle mir vor, dass Software in Zukunft sehr einheitlich und ganz grundsätzlich so organisiert sein sollte:

image

Die Grundbausteine von Software sind Komponenten. (Klassen sind auch wichtig, aber unterhalb der Wahrnehmungsschwelle von Softwarearchitekten.) Definieren wir sie einfach mal als binäre Codeeinheiten mit separater Leistungsbeschreibung (Kontrakt). Wenn Sie jetzt "Assembly" denken, liegen Sie nicht falsch.

Diese Komponenten "leben und arbeiten" in etwas, das ich mal Compartment nennen. Solche "Abteile" teilen einen Raum, deshalb finde ich den Begriff ganz passend in Bezug auf die Raum-Metapher, die hinter Application Space steht.

Wie diese Komponenten in den Abteilen aufgehängt sind, ist zunächst einmal nicht so wichtig. Hauptsache, sie haben erstmal ein "Zuhause", in dem manche "zusammen leben" und andere eben in anderen "Zuhauses". (Hm... vielleicht wäre "Appartment" besser als "Compartment"? Dann könnten Komponenten in einer WG leben :-)

Innerhalb der Compartments können Komponenten direkt miteinander kommunizieren, einfach über Methodenaufruf. Sie liegen im selben "Abteil" dicht beisammen (im selben Adressraum) und können daher den Stack nutzen. Alles ist, wie Sie es kennen und lieben. Einzig, die Verbindung zwischen zwei Komponenten wird durch einen DI Framework bzw. Microkernel hergestellt. Den bietet das Compartment als komponentenumfassender Kontext; das ist eine seiner Infrastrukturleistungen.

Wie Sie Compartments auf Prozesse oder Maschienen oder gar Netzwerke verteilen, ist zunächst einmal unerheblich. Das sind Feinheiten des Deployments. In einem AppSpace gilt: Was in einem Compartment gehostet wird, liegt nah beieinander in einem Adressraum, der direkte Kommunikation erlaubt. Deshalb können Sie auch entscheiden, ob Compartments das noch durch eigene AppDomains unterstreichen sollen. Kann sein, muss aber nicht.

Fangen Sie mit 3 Compartments in einem Prozess an und verteilen Sie sie später auf 3 Prozesse. Oder führen Sie Redundanz ein, indem Sie sie auf 7 Prozesse verteilen. Welcher Art die Prozesse sind - also IIS oder NT Service oder Console EXE usw. - ist dabei nicht so wichtig. Diese Compartment Hosts müssen einfach zum Zweck der darin "lebenden" Komponenten passen.

Wegen dieser Flexibilität fehlen im obigen Bild auch Prozesse und Maschinen und Netzwerke. Denn das Credo des AppSpace ist, dass diese Deploymenteinheiten zweitrangig sind. Erstrangig ist hingegen, ob eine Komponente synchron und nah bei anderen läuft oder asynchron und fern von anderen. Darüber gibt ein AppSpace-Diagramm jedoch Auskunft.

Synchrone Komponenten sind einfach - selbst wenn Sie DI benutzen. Asynchrone Komponenten jedoch... da wird es knifflig. Wie sollen die miteinander kommunizieren? Die Asynchronizität beginnt schon, wenn zwei Objekte auf separaten Threads laufen. Sie wird schlimmer, wenn diese Objekte in verschiedenen AppDomains hängen und noch übler wird´s, wenn sie durch Prozesse, Maschinenen oder Netzwerke getrennt sind.

Heute stehen dafür eine Reihen unterschiedlicher Technologien zur Verfügung. Da gibt es eine Pallette von Möglichkeiten, Parallelität einzuführen. Wenn Ihre Anwendung in mehrere Prozesse zerlegt ist, ergibt sich die ganz natürlich. Ansonsten können Sie Threads aber auch von Hand auf die eine oder andere Weise erzeugen. Aber Achtung: nicht zuviele, sonst ist die ganze Asynchronizität kontraproduktiv.

Vielfältiger sind die Möglichkeiten für die Kommunikation zwischen den Threads. Die kann über´s Dateisystem laufen oder gemeinsame Objekte (shared memory), rohe TCP Sockets funktionieren genauso wie WCF.

Diese Vielfalt der Kommunikationsmedien, so verständlich sie ist, halte ich nun inzwischen für nachteilig. Niemand kennt sich mehr aus. Ich plädiere daher für eine radikale Vereinfachung durch Vereinheitlichung, mit der sich vielleicht 80% aller verteilter Kommunikation lösen lässt. Nein, nicht 100%, aber ich sag mal optimistisch 80%. Mir liegt daran, Asychronizität "den Massen" nahezubringen und nicht wirklich jedem zu jeder Zeit die Entscheidung zwischen vielen Optionen aufzubürden.

Die Technologievielfalt kann und soll also erhalten bleiben - unter der Haube. Wer sie braucht, um im Einzelfall mal performanter zu sein, hat sie dann. Aber der allgemeine Fall wird durch Abstraktion einfacher. So wie es schon viele Male in der Geschichte der Softwareentwicklung geschehen ist. Beispiel SQL, Beispiel O/R Mapping, Beispiel GDI+ usw. usf.

Wie soll nun diese Vereinfachung der Kommunikation zwischen asynchronen Komponenten aussehen? Das obige Bild zeigt es: Neben der direkten Kommunikation innerhalb eines Compartments gibt es die indirekte (!) zwischen Compartments. Indirekt ist sie immer, wenn Komponenten asynchron zu anderen laufen. (Insofern kann auch innerhalb desselben Compartments indirekt kommuniziert werden.)

Indirekte Kommunikation bedeutet, dass eine Client-Komponente nicht (!) direkt mit einer Service-Komponente spricht. Client und asynchroner Service sind vielmehr immer (!) durch einen Vermittler verbunden. Im einfachsten Fall ist dieser Vermittler eine Warteschlange, eine Queue. Aber das ist nur der einfachste oder übliche Fall. Ein AppSpace ist nicht darauf beschränkt. Zwischen Client und Service können alle möglichen Vermittlerformen stehen: Queue, Stack, Baum, relationale Tabelle, Liste, Array, Dictionary... you name it.

Gemeinsam ist allen Vermittlern nur, dass sie Datenstrukturen sind. Die eine Komponente manipuliert sie, die andere beobachtet sie. Und dann können die Rollen wechseln. Beobachtung ist entweder aktiv (polling) oder passiv (Ereignisgesteuert, Stichwort: EDA).

Diese Datenstrukturen existieren virtuell im AppSpace und sind für alle Komponenten in allen Compartments grundsätzlich zugänglich. Deshalb liegen die Winkel der asynchronen Kommunikation im obigen Bild im Space.

Implementationstechnisch sieht das jedoch anders aus. Da liegen die Vermittlerdatenstrukturen ebenfalls in Compartments, sozusagen in einem Gürtel um sie herum.

image

Letztlich ist es aber egal, wo genau diese Datenstrukturen, die Container hängen. Sie können nach dem Prinzip "write remote, read local" immer beim asynchronen Service angesiedelt sein (also im Compartment der Pfeilspitze einer Verbindung zwischen zwei Komponenten). Oder sie können auch in einem ganz anderen Compartment liegen:

image

Container werden im AppSpace gegenüber Anwendungscode nicht durch eine Adresse repräsentiert, sondern durch einen AppSpace-globalen Namen.

Der AppSpace macht Komponenten insofern nicht nur durch DI implementationsunabhängig voneinander, sondern auch unabhängig in Bezug auf Ort und Zeit. Durch Container als Vermittler stehen Komponenten nicht mehr direkt in Kontakt, d.h. beide müssen nicht mehr gleichzeitig "anwesend" sein im AppSpace. Und AppSpace-weite Containernamen verbergen den genauen Ort (Compartment, AppDomain, Prozess, Maschine, Netzwerk), an dem eine asynchrone Komponente "lebt".

Nach der grundlegenden Entscheidung, ob Komponenten synchron oder asynchron kommunizieren sollen, kann die Topologie sich bei asynchronen Komponenten sehr weitreichend ändern, d.h. wechselnden Bedürfnissen anpassen, ohne dass Client-Anwendungscode davon betroffen wäre.

Der AppSpace soll also die Vorteile von EDA verallgemeinern (nicht nur Queues, sondern auch andere Datenstrukturen, auf die man lauschen kann) und mit Komponentenorientierung wie Computing in/via the Cloud verschmelzen. Ich sehe ihn als Aggregat vieler existierender Technologien, die er unter einer Abstraktionsdecke versteckt.

Insofern bietet ein AppSpace einen schönen Migrationspfad: Ich kann mit ihm anfangen und erstmal nur Komponenten in einem Compartment verbinden. Dann kann ich in die Asynchronizität klein einsteigen, indem ich mal im selben Compartment eine Komponente asynchron mache. Und dann kann ich die Asynchronizität ausdehnen auf mehr Komponenten. Und dann kann ich diese Komponenten verteilen. Schließlich laufen sie auf vielen Rechnern übers LAN oder gar Internet verteilt. Ohne, dass mein Applikationscode davon etwas merken würde. Denn sobald ich einmal mit der asynchronen, containervermittelten Kommunikation angefangen habe, skaliert die von der cross-thread bis zur cross-firewall Kommunikation. Das Geheimnis liegt darin, dass sie nicht nur nachrichtenorientiert, sondern eben datenstrukturvermittelt ist.

Mittwoch, 25. Juni 2008

Asynchrone Architekturen einfacher machen - Umriss eines Application Space

In meiner vorherigen Posting habe ich ja eine Lanze für mehr Asynchronizität gebrochen. Die halte ich für wünschenswert und notwendig - aber einfach herzustellen ist sie heute noch nicht. Man kann es, es gibt viele "Splittertechnologien", aber die sind nicht "at our fingertips".

Bezüglich asynchroner Architekturen sind wir noch in einer "vor-bequemen Zeit", also etwa wie bei der Persistenz so um 2002 herum. Da gab es noch keine O/R Mapper für .NET und kein noch einfacher zu nutzendes db4o oder Persistor.Net.

Der ThreadPool, der AsyncOperationManager, die CCR, WCF... diese und andere Technologien machen es uns heute schon einfacher, asynchron sowohl lokal wie verteilt zu arbeiten. Dennoch: insgesamt sind sie mir noch zu kompliziert. Das liegt zum Teil an den APIs - die wollen jeder für sich one-size-fits-all Lösungen sein -, andererseits liegts am Konzept dahinter. WCF mag zwar nachrichtenorientiert sein - sehen tut man das aber nicht wirklich. Da helfen auch keine Data Contracts.

Um die gläserne Decke der Synchronizität nicht zu durchbrechen, sondern sie schlicht auf dem Weg zu mehr Performance und Skalierbarkeit zu umgehen, wünsche ich mir deshalb eine Infrastruktur, die Asynchronizität viel einfacher macht. Hier mal ein paar Punkte, die sie für mich minimal erfüllen müsste:

  • Einfache lokale Kommunikation zwischen Komponenten; Stichworte: Contract-first Design, Dependency Injection und Microkernel
  • Einfache verteilte Kommunikation; Stichworte: Nachrichtenorientierung, Event-Driven Architecture, "Datenstrukturorientierung" statt Serviceorientierung
  • Einfache Änderung der Topologie von Netzen verteilter Komponenten; Stichworte: Service Discovery, Unterstützung verschiedener Transportprotokolle, location transparency
  • Einfache Verteilung "in the cloud"; Stichworte: Tunnelung von NAT und Firewalls
  • Einfaches Hosting von Komponenten in unterschiedlichen AppDomains
  • Einfache Verteilung der Arbeit auf mehrere Threads

Dahinter stehen für mich ein paar Ansprüche, wie ich arbeiten/denken können möchte:

- Ich glaube, dass echte Komponentenorientierung durch Infrastruktur unterstützt werden muss. Sie stellt sich nicht von allein ein, sondern muss quasi durch Technologie "nahegelegt" werden. Visual Studio oder der .NET Framework tun das bisher nicht.

- Eine komponentenorientierte Infrastruktur soll das Laden von Komponenten einfach machen und auch die Kommunikation zwischen Komponenten.

- Bei der Komponentenorientierung möchte ich zunächst nicht zwischen lokalen und entfernten Komponenten unterscheiden. Beide sollten also durch dieselbe Infrastruktur unterstützt werden.

image - Für die Kommunikation zwischen Komponenten glaube ich allerdings fest daran, dass sie erstens zwischen lokalen und verteilten Komponenten ganz anders (!) aussehen sollte (s. dazu auch meinen Beitrag in "SOA Expertenwissen"). Und zweitens glaube ich, dass sie für verteilte Komponenten viel einfacher und einheitlicher werden sollte, als mit .NET Remoting oder auch WCF der Fall. Diese Einheitlichkeit und Einfachheit mag dann nicht 100% aller Probleme lösen, aber 80% sollten damit viel leichter zu erschlagen sein.

- Verteilte Komponenten möchte ich genauso einfach auf verschiedenen Threads wie in verschiedenen AppDomains oder auf verschiedenen Rechnern laufen lassen. Selbst wenn das Internet - the cloud - zwischen ihnen stehen sollte, darf sich nichts an ihrer Kommunikation ändern.

- Für einfache Verteilung ist es auch wichtig, dass ich die Wahl habe zwischen Push und Pull, um an Informationen zu kommen. Ich möchte selbst Last ganz einfach verteilen können, also nicht auf eine Cluster-Infrastruktur angewiesen sein. Warum kann ich nicht einfach heute einen Rechner als Server irgendwo in der Welt hinstellen und morgen 2, 3, 4 weitere irgendwo anders in der Welt - und alle teilen sich die Last? Klar, das geht schon... aber nicht so einfach. Da kann ich nicht nur schnell mal 'nen Service Contract mit WCF aufsetzen. Solche Flexibilität möchte ich aber haben.

Bottom line: Für mich müssen "Komponentendenke", Verteilung auf kurze und weite Distanz und Parallelität in einem einfachen 80%-Werkzeug zusammenkommen. Dabei gehts nicht darum, ein Rad neu zu erfinden, sondern vorhandene Ränder (endlich) zu einem Auto für jedermann zusammenzustecken. Bekanntes, Vertrautest, Bewährtes soll in Synergie zusammenarbeiten und in der Summe mehr sein, als die Teile vermuten lassen.

image Alles beginnt dabei mit... Einfachheit. Wir brauchen heute nicht leistungsfähigere Technologien, sondern vor allem einfachere. Sonst bleiben zuviele Entwickler zurück. Sie haben nicht die Zeit, sich in Kompliziertes einzuarbeiten. Kommen die Aspekte Komponentenorientierung, Verteilung und Parallelität nicht zusammen, dann nutzen nur wenige das Potenzial, das in Multi-Core Prozessoren und dem universalen Internet steckt.

Mit einem Application Space (AppSpace) wie oben skizziert, sähe das jedoch anders aus. (Ich könnte ihn auch Integration Space oder Component Space nennen. Application Space klingt für mich aber im Augenblick handlicher, weniger out-spaced ;-) Er wäre für komponentenorientierte, asynchrone Software das, was db4o für die Persistenz ist: ein "Ermöglicher" (enabler), der für viele erstmals etwas in Reichweite rückt, was ihnen bis dahin akademisch oder schwierig erschienen oder gar unbekannt war.

Dienstag, 24. Juni 2008

Synchronizität - Die gläserne Decke der Softwareentwicklung

Im Grunde kann man alle Probleme durch synchrone und sequenzielle Verarbeitung lösen. Das dauert dann manchmal zwar ein wenig, aber es geht. So bringt es uns auch jeder Programmierkurs bei. Ein Befehl folgt auf den anderen. Ein Unterprogramm ruft das nächste und wartet auf dessen Ergebnis, bevor es selbst weitermacht. Unterprogramme sind insofern fast nur syntactic sugar, denn ihr Code könnte auch am Aufrufort eingesetzt stehen. Compiler tun auch genau das, wenn es ihnen angemessen erscheint. Das heißt dann Inlining.

Was aber, wenn so eine sequenzielle Verarbeitung zu langsam ist? Na, dann macht man sie halt schneller, indem man den Algorithmus verbessert (z.B. Quick Sort statt Bubble Sort) oder einen schnelleren Prozessor kauft. Das ist dann so, als würde man einen schmächtigen Maurer ins Fitnessstudio schicken, damit er in Zukunft schneller Steine schleppen kann. Oder man ersetzt ihn gleich durch ein Förderband.

Wie effizient kann eine Implementation aber werden? Irgendwann ist der beste sequenzielle Algorithmus implementiert. Wie schnell können Prozessoren werden? Irgendwann ist da eine physikalische Grenze erreicht - wie es gerade so um die 3-6 GHz zu sein scheint.  Genauso ist es mit der Kraft bei Maurern und der Schnelligkeit von Föderbändern. Da gibt es einfach ein Ende der Fahnenstange. Dann hilft nicht mehr Quantität. Nein, dann muss sich die Qualität ändern.

Angekommen ist das in der Programmierausbildung aber eher noch nicht, würde ich sagen. Denn es herrscht immer noch das Denken in synchronen und sequenziellen Abläufen vor, die man eben versucht durch quantitative Einflussnahme zu beschleunigen. Weniger Instruktionen oder schnellere Prozessoren stehen als Wunschvorstellung hinter den Klagen darüber, dass mal wieder der Aufbau eines Fensters zu lange dauert oder die Datenbank zu langsam ist.

image Dabei wussten schon die alten Ägypter, dass man Pyramiden nicht schneller baut, indem man die Arbeiter erstmal ins Bodybuilding-Studio schickt. Die Pyramiden wurden von normalen Einwohnern Ägyptens gebaut. Genauso wie der Otto Versand vor allem Durchschnittsmenschen zur Bewältigung aller durch Menschen durchzuführenden Tätigkeiten einstellt. Sowohl die Ägypter wie der Otto Versand müssen halt damit leben, dass es vor allem normale Menschen gibt. Das liegt in der Definition von normal. Exzeptionelle Menschen - besonders starke, schnelle oder kluge - gibt es nur vergleichsweise wenige. Man kann sie daher nicht in größerer Menge rekrutieren und schon gar nicht bezahlen.

Die Lösung für Geschwindigkeitsprobleme liegt daher nicht im scale-up, in der Effizienzsteigerung von etwas Einzelnem, d.h. einem Arbeiter, einem Förderband, einem Rechner.

Die wahre Lösung liegt vielmehr in der Parallelisierung. "Normale" Ressourcen gleichzeitig an einer Aufgabe arbeiten zu lassen - z.B. viele normale Ägyptische Bauern an einer Pyramide oder Hamburger Bürger beim Otto Versand -, ist letztlich der weiter führende Weg, um dauerhaft schnell zu sein.

Beim scale-out stehen viele billige Ressourcen nebeneinander und arbeiten parallel an der Bewältigung einer Gesamtaufgabe. Statt viel Zeit in die Optimierung von Algorithmen zu stecken oder viel Geld in eine noch schnellere Hardware, sollten Sie überlegen, inwiefern die abzuarbeitenden Schritte in Ihrer Software parallelisierbar sind.

Das geht nicht, glauben Sie, weil Sie keine riesige ERP-Software für komplexe Geschäftsprozesse schreiben? Sie entwickeln doch nur ein kleines CRM - bei dem allerdings der Programmstart und auch der Wechsel zwischen Bearbeitungsmaske und Suchdialog an Performance zu wünschen übrig lassen.

Aber warum soll den nicht auch in solcher Software, die scheinbar nur aus sequenziellen, synchronen Verarbeitungsschritten besteht, etwas Parallelität möglich sein?

  • Beispiel Programmstart: Alles, was beim Programmstart nicht visuell ist (z.B. das Laden von Lookup-Daten), kann parallel zur Anzeige eines ersten Dialogs im Hintergrund passieren.
  • Beispiel Suchen: Abfragen können im Hintergrund laufen, während das User Interface weiterhin bedienbar bleibt. Abfrageergebnisse können häppchenweise ans UI geliefert werden; niemand sollte darauf warten müssen, dass auch noch der tausendste Datensatz dem Grid hinzugefügt ist, bevor er weiterarbeiten kann.
  • Beispiel Bearbeitungsende: Wenn am Ende der Bearbeitung Daten zu speichern sind, dann kann das parallel zur weiteren Bedienung geschehen. Warum sollte ich als Anwender darauf warten, dass die Daten auch vollständig und korrekt in der Datenbank angekommen sind? Das ist doch selbstverständlich.

Falls Sie diese Beispiele nicht überzeugen, biete ich an, dass ich Ihre Anwendung daraufhin untersuche, wo es Parallelisierungspotenzial gibt. Ich bin sicher, da gibt es einiges zu entdecken.

Warum nutzen wir das große scale-out Potenzial denn aber nicht in unseren Anwendungen? Gelegentlich mag es daran liegen, dass sich ein Algorithmus nicht so leicht parallelisieren lässt. Das halte ich aber für ein Detailproblem. Vor allem sehe ich den Grund nämlich in einem Mangel an 1. Bewusstsein und 2. einfacher (!) Technologie.

Sie können Programmteile heute selbstverständlich parallel machen. Asynchronizität ist natürlich möglich. Aber sie ist trotz aller Bemühungen der .NET-Framework-Entwickler noch nicht wirklich, wirklich einfach. Auch sind die Lösungen verteilt: ein bisschen steckt in System.Threading, ein bisschen in der CCR, ein bisschen in WCF usw.

Ich halte daher eine Konsolidierung für geboten. Das, was an Technologieschnipseln vorhanden ist, sollte unter einem intuitiven, einfach einzusetzenden Dach zusammengefasst werden. Dazu müsste dann aber wohl noch etwas Theorie kommt. Wir müssen Software erstmal aus grundsätzlich asynchronen Bausteinen zusammengesetzt denken.

image Aber ich sehe Licht am Horizont... :-) Mit einem Architekturansatz wie "Systemorientierten Programmierung" (SOP) und einer Zusammenführung von DI Frameworks mit Enterprise Service Bus (ESB) und Space Based Computing/Architecture (SBC/SBA) könnte es besser werden. Bei SOP stehen Softwarezellen als asynchrone und verteilte Codeeinheiten am Anfang, so dass das Denken in Richtung mehr Parallelität und flexibler Architekturen geleitet wird. Und eine Vereinigung von EDA-Konzepten (Event Driven Architecture) könnte das Hosting und die Kommunikation von Softwarezellen vereinfachen - im Großen wie im Kleinen (innerhalb von Prozessen).

Aber davon ein andermal... Einstweilen Achtung vor der gläsernen Decke "Synchronizität"! Synchrone Verarbeitung durch scale-up zu beschleunigen, führt nicht immer so weit, wie Sie denken mögen.

Samstag, 16. Februar 2008

Ordnungsaspekte für das Multithreading [OOP 2008]

Auf der VSone 2008 habe ich gerade einen Vortrag über Software Transactional Memory (STM) gehalten und meine Open Source Implementation NSTM vorgestellt. Der Vortrag war gut besucht; das hat mich sehr gefreut und ermutigt, weiter an NSTM zu arbeiten. Ein wenig Hilfe von anderen Entwicklern wäre dabei natürlich schön... Bei den Collections, die für einen STM neu entwickelt werden müssen, ist einiges zu tun. (Vom .NET Fx angebotene Collections wie List<T> oder Queue<T> können nicht einfach "von außen" transaktional gemacht werden, da ihre internen Strukturen nicht auf STM basieren. Nur eine grundsätzliche thread-safety lässt sich von außen durch pauschale Sperren herstellen.)

Nun finde ich Ordnung/Überblick immer gut. Deshalb habe ich mich gefreut, beim Blick auf die VSone Agenda eine persönliche Erkenntnis gehabt zu haben. Da war nämlich nicht nur ich mit einem Multithreading-Vortrag vertreten, sondern auch Bernd Marquardt zum Thema OpenMP. Dazu kommt noch, dass ich neulich ein wenig über Microsofts Concurrency Coordination Runtime (CCR) gelesen hatte.

Meine Erkenntnis war nun, dass ich in diesen/zwischen diesen Technologien ein Muster erkannt habe. Und Muster als Strukturierungen der Realität finde ich immer gut. Die erzeugen in mir immer ein Gefühl von Entspannung und Aha nach dem Motto "Achsoooo, so ist das! Eigentlich ganz einfach..." :-)

Das Muster, das ich nun gesehen habe, betrifft die Einteilung von Multithreading-Themen. Herb Sutter hat ja auch schonmal eine Einteilung in "The Pillars of Concurrency" gemacht. Die gliedert die Parallelverarbeitung nach Zwecken. Das ist aber nur eine mögliche Dimension, finde ich. Eine andere ist die Einteilung nach "technischen Aspekten".

Für mich teilt sich Multithreading danach auf in drei grundsätzliche Problembereiche:

  1. Code auf Threads verteilen: Code in einem eigenen Thread laufen zu lassen, kann so einfach sein, wie ThreadPool.QueueUserWorkItem() aufzurufen. Zum ersten Aspekt des Multithreading gehört aber mehr. Denn Code explizit auf Threads zu verteilen ist letztlich umständlich oder gar eine intellektuelle Herausforderung. Viel effizienter wäre es doch, wenn Sie sich über die Parallelisierung von Code gar keine Gedanken machen müssten. Mit implizitem oder zumindest deklarativem Multithreading wäre Ihnen eine Last genommen. OpenMP oder Active C# sind dafür hilfreiche Technologien. Mit ihnen erzeugen Sie Threads nicht mehr explizit im Code, sondern sagen viel pauschaler "Hier soll etwas automatisch parallel laufen. Irgendwie."
  2. Parallelen Code koordinieren: Paralleler Code läuft selten isoliert. Er muss vielmehr in seinen Aktivitäten koordiniert werden. Threads müssen sich abstimmen, in dem was sie tun. Sie wollen einander Aufforderungen schicken oder aufeinander warten. Der .NET Fx bietet dafür z.B. WaitHandle verschiedener Art. Aber andere Technologien machen es noch einfacher, z.B. die Ports der CCr oder Protokolle in Active C#. Ganz allgemein geht es bei der Koordinierung immer um Signale in Warteschlangen, auf die man warten kann.
  3. Gemeinsame Ressourcen parallel nutzen: Wenn Threads dann laufen und sich koordinieren, womit arbeiten Sie dann? Häufig auf gemeinsamen Ressourcen. Die können sie aber nicht einfach so ohne Beachtung der anderen Threads egoistisch nutzen! Parallele Nutzung gemeinsamer Ressourcen läuft immer Gefahr, Inkonsistenzen zu erzeugen  - wenn sie sich nicht koordiniert. Koordinationstechnologien helfen also bei der gemeinsamen Ressourcenutzung. Aber eigentlich ist explizite Koordination genauso ineffizient wie explizite Parallelisierung. Auch hier ist also Abhilfe gefordert. Software Transactional Memory oder auch Space Based Collaboration sind Ansätze, die die Parallelnutzung leichter machen. Sie bieten Abstraktionen, hinter denen konkurrierende Zugriffe und ihre explizite Koordination verschwinden.

Wenn ich zukünftig irgendwo etwas über Multithreading lese, dann werde ich mich sofort fragen, um welchen dieser drei Aspekte es dabei geht. Irgendwie ordnet sich für mich dadurch das Thema besser als bisher.

Und ich habe damit Oberbegriffe in der Hand, die ich auch auf Herb Sutters Zwecke anwenden kann. Wie ist´s z.B. mit der Skalierbarkeit? Welche Aspekte haben Einfluss auf sie? Größere Skalierbarkeit ergibt sich z.B. immer durch weniger Koordination und weniger gemeinsame Ressourcen. Oder wie ist´s mit Responsiveness? Dafür ist es vor allem wichtig, Code einfach parallelisieren zu können. Dann nutze ich Parallelität nämlich eher, um responsive zu bleiben.

Würde mich freuen, wenn diese Aspekte Ihnen auch ein wenig helfen würden, die Multithreading-Welt "geordneter" zu sehen.