Wettrennen

Probleme mit der asynchronen Verarbeitung verhindern
Kommentare

Das „A“ in AJAX steht bekannterweise für „Asynchron“, also das mehr oder minder parallele Verarbeiten von Anfragen, ohne jedoch beim Warten auf die Antwort die sonstige Arbeit einzustellen. Was sich hier so positiv anhört, kann jedoch in der Praxis auch schnell zu Problemen führen, wenn man die Gefahren der asynchronen Verarbeitung außer Acht lässt. 

Wer mit AJAX arbeitet, möchte in der Regel die Kommunikation mit dem Server im Hintergrund stattfinden lassen, die dem Besucher gezeigte Seite dynamisch beim Eintreffen der Antwort verändern und somit Overhead in Form unnötiger Seiten-Reloads verhindern. Ein durchaus lobenswerter Ansatz eigentlich, doch weiß die Backend-Anwendung um dieses Vorgehen? Kommt das Frontend mit Verzögerungen klar? In diesem Artikel werden wir uns anhand kurzer Beispiele mit den möglichen Fallstricken der asynchronen Verarbeitung beschäftigen und versuchen, praxisrelevante Lösungen zur Vermeidung der Probleme zu finden.

Problemfall 1: Intervall

Einer der vermutlich häufigsten Anwendungsfälle von AJAX-Entwicklungen ist das Überwachen von Ereignissen bzw. das Warten auf das Eintreten eines solchen. Anstelle einer vermeintlich ewig auf sich warten lassenden Antwort des Servers wird dem Benutzer eine „In-Progress“-Seite präsentiert, im Idealfall sogar eine Fortschrittsanzeige implementiert. In der Praxis wird dies meist mithilfe eines setInterval()-Aufrufs umgesetzt, der in regelmäßigen Abständen den neuen Status der Verarbeitung vom Server abruft. Soweit eigentlich kein Problem, oder? Doch! Denn was passiert eigentlich, wenn die Zeit, die der Server aus irgendwelchen Gründen zum Übermitteln der Daten braucht, größer ist als das Zeitfenster des Intervalls?

Sofern die Möglichkeit einer Verzögerung nicht explizit im JavaScript-Code berücksichtigt wird, haben wir eine klassische Race-Condition: Während noch auf das Ergebnis der vorherigen Anfrage gewartet wird, starten wir bereits eine weitere. Sofern unsere JavaScript-Implementierung versucht, ökonomisch zu sein und daher mit nur einer Instanz des XMLHttpRequest-Objekts arbeitet, fällt unsere Anwendung bereits auf die Nase.

Werden hingegen individuelle Instanzen eingesetzt, stellt sich gleich die nächste Frage: Kommt unsere Anwendung mit der in keiner Form garantierten und daher vermeintlich willkürlichen Reihenfolge klar, in der Antworten vom Server eintreffen? Nur weil wir eine Anfrage zuerst gestellt haben, muss diese ja noch lange nicht als Erstes auch beantwortet sein. Im Falle eines Fortschrittsbalkens wäre es also beispielsweise nur dann sinnvoll, feste Prozentwerte für den aktuellen Stand der Bearbeitung zu übermitteln, wenn im Frontend sichergestellt ist, dass es zu keinen Überschneidungen der Anfragen/Antworten kommt; ansonsten würde die Länge des Balkens unter Umständen lustig hin- und herspringen, anstatt kontinuierlich zu wachsen. Eine Möglichkeit diese Kontinuität zu garantieren, wäre über einen Schalter (z.B. eine boolesche Variable) anzuzeigen, dass gerade noch eine Anfrage unbeantwortet ist und man somit keinen neuen Request abzuschicken braucht. Etwas eleganter erreichen kann man dies aber auch, indem man anstelle von setInterval() mit setTimeout() arbeitet und den jeweiligen Handler den Aufruf explizit erneut setzen lässt: xhttp.onreadystatechange=function() { ….  window.setTimeout(‘update()‘,X); … }. Problem Nummer eins wäre damit gelöst.

Problemfall 2: Abhängigkeiten

Ein weiteres bekanntes Szenario dürfte die Abhängigkeit einzelner Requests voneinander darstellen: Das Ergebnis eines zweiten Prozesses darf erst dann verarbeitet werden, wenn der erste ebenfalls erfolgreich abgeschlossen wurde. Dies wird jedoch spätestens dann zu einem „Problem“, wenn man Daten aus verschiedenen Quellen beziehen will und das Ganze daher eigentlich perfekt parallel abrufen könnte (wenn man denn sicher wäre, dass die Reihenfolge der Antworten passt).

Sofern sich der zweite Aufruf nicht gerade aus Daten generiert, die erst durch den vorherigen eingeliefert werden, was quasi zwangsläufig eine sequenzielle Abarbeitung bedingen würde, kann man sich mit einem „Warte-Thread“ behelfen. Die jeweiligen Load-Handler erhöhen einen „fertig“-Zähler jeweils um den Faktor eins, mittels einer unabhängigen, via setInterval() regelmäßig ausgeführten Funktion prüft man nun, ob dieser Zähler bereits die gewünschte Höhe erreicht hat. Ist dies der Fall, schaltet man das Intervall einfach wieder ab und führt die eigentliche Zielfunktion aus, die jetzt auf die Daten von beiden Aufrufen zurückgreifen kann. Dies lässt sich selbstverständlich auch mit drei und mehr Parallelprozessen abbilden, wobei es zu berücksichtigen gilt, dass die meisten Browser mehr als zwei parallele Zugriffe auf die gleiche Domain nicht unterstützen, und somit das Ganze implizit sequenziell ausgeführt wird.

Problemfall 3: DOM

Nachdem wir jetzt relativ zielsicher in der Lage sind, Überschneidungen bei den Anfragen und Antworten abzufangen bzw. zu verhindern, bleibt ein weiteres Feld. Bekanntermaßen Ziel fast aller AJAX-gestützten Anwendungen ist es, dynamisch die aktuell dargestellte Seite passend zum Ergebnis des Requests und unter Zuhilfenahme von DOM-Funktionen umzugestalten. Auch dies ist erst auf den zweiten Blick mit möglichen Problemen behaftet. Denn was passiert eigentlich, wenn zwei Prozesse simultan im DOM Umstrukturierungen vornehmen? Und was ist, wenn ein Prozess ein Objekt löscht, während der andere gerade versucht den Inhalt zu ändern?

Die meisten dieser Fragen lassen sich unter Einsatz der obigen Lösungswege schon von vornherein als erledigt abhaken. Dennoch kann es in der Praxis vorkommen, dass zwei Funktionen sehr zeitnah ausgeführt werden, die Änderungen am DOM durch die erste Funktion aber intern noch nicht abgeschlossen sind. Dies wird besonders dann problematisch, wenn es sich – wie beim Internet Explorer – um eine sehr langsame Implementierung handelt und man beim Umbau nicht sehr weitsichtig vorgeht. Um sich das umständliche händische Erzeugen ganzer Teilbäume zu ersparen, erzeugen viele via cloneNode(true) eine Kopie eines Teilbaums im Speicher. Sind in diesem Teilbaum Elemente mit gesetzten ID-Attributen und fügt man diese Kopie nun an der gewünschten Stelle im DOM ein, ist die DOM-Struktur an sich zwar noch „heil“, dank der jetzt doppelten IDs jedoch nicht mehr zuverlässig verwendbar. Greift jetzt, während man selbstverständlich innerhalb der ersten Funktion die noch ausstehenden Aufräumarbeiten durchführt, eine weitere Funktion auf das DOM zu, ist das daraus resultierende Ergebnis nur bedingt vorhersagbar. Um diese Probleme zu vermeiden, könnte man natürlich ebenfalls mit Flags arbeiten, die einem Prozess signalisieren, dass hier noch gearbeitet wird und man bitte warten möge. Oder man arbeitet gleich sauber: Anstatt die geklonten Objekte direkt wieder in den Tree zu hängen, räumt man erst auf, setzt die richtigen Attribute und fügt erst das gesäuberte Objekt zurück in die Seite ein.

Arne Blankerts ist Leiter der Entwicklung bei der SalesEmotion AdSolutions GmbH in Hamburg.
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -