Hundespaziergänge unbekannter Länge (Teil 1)

Async und Await richtig anwenden
Kommentare

Wenn meine Frau Adriana unseren Hund Aslan spazieren führt, dann hat das genau was mit Asynchronität zu tun? Und was, bitteschön, ist ein InkCast? Geduld! Ich kann beides erklären. Bevor Sie aber mit der Lektüre dieses InkCasts beginnen, schnappen Sie sich Ihr Tablet bzw. Smartphone und rufen Sie schon mal die YouTube-Seite auf – Sie werden sie nämlich gleich brauchen.

Selten hat es ein neues Sprachfeature in Visual Basic und in C# gegeben, das einerseits so nützlich, andererseits aber auch so problematisch gewesen ist wie Async/Await. Um dieses Feature ranken sich – leider – mehr Mythen und Halbwahrheiten, als es dazu wirklich brauchbare Informationen gibt. Das geht schon damit los, dass viele Entwickler Asynchronität und Parallelisierung in einen Topf schmeißen – dabei ist das parallele Ausführen von Code mithilfe von Threads oder Tasks nur ein Aspekt der asynchronen Programmierung, denn in den meisten Fällen geht es ohne. Weiter geht es damit, dass vielen Entwicklern der Unterschied zwischen CPU-Bound-Code und I/O-Bound-Code, oder, übersetzt, zwischen CPU-lastigem und Peripherie-lastigem Code gar nicht bewusst ist.

Wenn es um das Spazierengehen mit unserem Hund Aslan geht, ist meine Frau Adriana diejenige, die das mit wachsender Begeisterung praktiziert. Nicht dass ich nicht ebenfalls gerne mit ihm meine Runden drehe, aber meine sind in der Regel ungleich kürzer. Und was den Zeitaufwand anbelangt, bin ich fast schon dankbar, dass Laufen nicht so meine Domäne ist und sie ausreichend oft meinen Part übernimmt.

Was ich nicht mache (und auch ähnlichen Blödsinn versuche ich zu vermeiden …): Adrianas asynchrone Tätigkeit des Aslan-Ausführens wartend vor der Tür zu verbringen, denn zeitlich gesehen würde mir das offensichtlich nicht viel bringen. Fragen Sie sich, wieso ich überhaupt auf solch ein sinnfreies Szenario komme? Wieso sollte jemand nach dem Schließen der Tür vor dieser stehen bleiben, und nichtstuend darauf warten, dass die beste Ehefrau von allen nach einer halben, oder einer, oder auch zwei Stunden (a-synchron eben, man weiß also nicht, wie lange es dauert: a-synchron bedeutet lediglich „zeitlich unabgesprochen, nicht koordiniert“) wieder zurückkehrt? Ganz einfach: Weil ich die Wette, dass Sie es beim Entwickeln garantiert schon gemacht haben, totsicher gewinnen werde, und das möchte ich Ihnen im Folgenden nicht nur demonstrieren, sondern ich möchte Sie gleichzeitig aufklären, was es mit dem schon angekündigten InkCast auf sich hat: Normalerweise untermalt man ja einen Artikel wie diesen hier getreu dem Motto „Ein Bild sagt mehr als tausend Worte“ mit entsprechenden, den Sachverhalt näher erklärenden Screenshots. Wie ich aber finde, sind Screenshots sooo Neunziger, irgendwie altbacken, sodass die folgende Idee entstand: Kleine YouTube-Clips, die bestimmte Sachverhalte ein wenig genauer demonstrieren, können es doch noch viel besser rüberbringen, so nach dem Motto: Ein YouTube-Video sagt mehr als tausend Bilder! Und deswegen haben Sie ja nun auch Ihr Tablet internet- bzw. YouTube-bereit vor sich liegen, und Sie können sich den ersten Clip anschauen, der demonstriert, was ein typischer, nahezu täglich angewandter Allerweltsprogrammierstil mit dem Warten hinter der Haustür auf die Rückkehr der besten Ehefrau von allen gemeinsam hat.

Sie finden den WebCast auf YouTube, indem Sie entweder den untenstehenden Link eingeben, oder sich über den QR-Code (Abb. 1) mithilfe ihrer Tablet- oder Smartphonekamera direkt zum Video führen lassen – viel Spaß beim ersten Teil!

Abb. 1: http://youtu.be/rrnFRPlfMXg

So, Video geschaut? Eigentlich sollten Sie jetzt eine Reihe von Fragen haben. Zur ersten Beispiel-App: Ihnen ist sicherlich aufgefallen, dass die Prozessorleistung nie über 25 Prozent gestiegen ist, obwohl wir den Prozessor durch den Beispielcode bei der Berechnung von Primzahlen durchgängig voll auslasten. Warum ist das der Fall? Und was war eigentlich genau der Grund dafür, dass sich die App während der Ausführung nicht mehr bedienen ließ? Zur zweiten App: Die Symptome hier waren exakt die gleichen wie bei der ersten App – auch sie ließ sich nicht mehr bedienen. Doch ist Ihnen hier nicht ein großer Unterschied aufgefallen? Richtig! Auch wenn sich die zweite App beim Schreiben auf den Memory-Stick quasi gleich verhalten hat – hier war so gut wie keine Prozessorauslastung zu sehen. Während man also beim ersten Beispiel noch argumentieren könnte, der Prozessor war so mit dem Primzahlenberechnen beschäftigt – die Prozessorauslastung im Task-Manager hat dies ja eindrucksvoll gezeigt –, funktioniert dieser Erklärungsansatz im zweiten Beispiel gar nicht. Der Prozessor hat nur am Anfang einmal kurz gezuckt. Danach wurden die 100 Megabytes quasi durch Geisterhand auf den Stick geschrieben; laut Task-Manager war der Prozessor dabei nicht beteiligt.

Zur Auflösung: Die Primzahlenberechnung des ersten Beispiels lief auf einem so genannten Thread. Thread ist das englische Wort für Faden und gemeint ist hier – bildlich gesprochen – quasi der Faden, der sich durch den Programmverlauf zieht. Wie der sprichwörtliche rote Faden werden die Methoden und Anweisungen nacheinander ausgeführt, sodass sich eine Art Ablaufpfad durch den Programmcode ergibt. Moderne Prozessoren können aber nicht nur einen Faden zur gleichen Zeit durch das Programm ziehen lassen, sondern zig davon, sogar Hunderte. In diesem Fall bekommt jeder der gestarteten Fäden eine Weile Zeit, seinen Programmverlauf abzuarbeiten – in Windows gilt: Wenn keine multimedialen Dinge abgespielt oder erzeugt werden (etwa Videos oder Spiele oder auch sonstige grafikintensiven Anwendungen, die schnelle Timer oder Videobildwiederholfrequenzen benötigen), dann bekommt jeder Faden (jeder Thread) ca. 22 ms Zeit, um ein paar Anweisungen auszuführen. Naja, es sind gar nicht so wenige Anweisungen, die der i5-Prozessor im Surface 3 in 22 ms ausführen kann. Er kann in dieser Zeit beispielsweise innerhalb eines Threads eine Integervariable auf über 100 000 hochzählen und dabei auch noch überprüfen, ob die 22 ms bereits erreicht wurden. Übrigens: 22 ms sind für das Synchronhalten bestimmter Dinge manchmal einfach zu wenig, deswegen gilt: Wenn multimediale Dinge auf einem Rechner stattfinden, dann wechseln die Threads bereits nach einer Millisekunde.

Dinge, die uns beim Computer als gleichzeitig laufend erscheinen, müssen das also gar nicht sein: Sie werden so schnell im Wechsel ausgeführt, dass sie einen quasi gleichzeitigen Eindruck vermitteln. Mit den neuen Prozessoren der letzten zehn Jahre hat die Multi-Core-Technik beim Prozessorbau Einzug gehalten: Ein Prozessor vereint dabei mehrere Prozessor-Cores unter einem Dach, von denen jeder mindestens einen, durch eine besondere Technik bei den Intel-Prozessoren („Hyper-Threading“) sogar zwei Threads wirklich gleichzeitig ausführen lassen kann. Ein i5-Prozessor ist ein solcher Dual-Core-Prozessor, und durch seine Hyper-Threading-Fähigkeiten kann er tatsächlich vier Threads gleichzeitig ausführen. Das bedeutet aber auch: Wenn wir nur einen Thread auf einem Core nutzen, dann nutzen wir – vereinfacht gerechnet – nur ein Viertel, also 25 Prozent der Prozessorleistung. Und das ist exakt der Wert, den Sie im Task-Manager im Video beobachten konnten. Und was ist nun der Grund, dass das Programm während des Programmverlaufs quasi einfriert? Das liegt nicht nur daran, dass es einen so genannten UI-Thread (wörtlich: Benutzeroberflächenfaden) gibt, der sich, der Name sagt es schon, darum kümmert, dass die Anwendung bedienbar ist. Auf dem UI-Thread sollten die (und eigentlich nur die) Dinge getan werden, die dafür sorgen, dass die Benutzeroberfläche bedienbar ist und bleibt. Was wir jedoch im ersten Beispiel getan haben (und was leider viel zu viele Programme viel zu oft machen) ist, den UI-Thread für eine lange, intensive Berechnung zu zweckentfremden. Im Grunde genommen haben wir ihn sogar gekapert, und Windows musste sogar einen Notfallplan zur Anwendung kommen lassen, um Schlimmeres zu verhindern. Und das nur, weil wir unsere Anwendung nicht richtig konzipiert haben. Um zu verstehen, wie ein Windows-Programm im Innersten funktioniert und wieso es wichtig ist, dass der UI-Thread „frei“ bleibt, können Sie sich ein weiteres Video anschauen (Abb. 2).

Abb. 2: https://www.youtube.com/watch?v=7_HMkjTPy0k

Meines Erachtens wird jetzt leicht klar, was das Problem des ersten Beispiels ist. Wenn wir innerhalb eines Button-Click-Events unsere Primzahlen berechnen lassen, kann das Programm die Windows-Nachrichten nicht mehr verarbeiten, und es passiert, was passiert. In Windows-Forms-Anwendungen sind Anwender ein solches Verhalten leider seit Anbeginn an gewohnt – in modernen Apps aus Windows Store, Windows Phone oder auch iOS und Android sind das Dinge, die nicht mehr akzeptiert werden. „Fast and fluent“ muss es hier sein, „schnell und flüssig“, es darf nicht ruckeln oder hängen, da sonst das Toucherlebnis beim Bedienen einer App auf der Strecke bleibt. Der Grundsatz lautet hier: Keine UI-Thread-Operation darf länger als 50 ms dauern, sonst schränkt das nicht nur das Bedienerlebnis für den Anwender spürbar ein – unter Umständen wird Ihre App gar nicht erst im Store zugelassen. Muss man also für alle langwierigen Operationen einen zweiten Thread erstellen, damit eine Anwendung schnell und flüssig bleibt? Die Antwort mag sie überraschen, denn das macht man nur in Ausnahmefällen, und damit kommen wir zum zweiten Beispiel. Sie haben im Video gesehen, dass diese zweite App wie die erste während des Schreibens auf dem Stick quasi hing, im Gegensatz zum ersten Beispiel aber gar keine Prozessorlast verursachte. Das lag daran, dass der Prozessor mit dem Schreiben auf den Stick an sich nicht viel zu tun hat. Vom Prinzip her sagt er einem bestimmten Chip im Computer, der so genannten South Bridge, welchen Speicherbereich er auf den USB-Stick schreiben soll, startet den Prozess, ja, und dann wartet der Prozessor, bis der Schreibvorgang beendet wurde. Und was tut er in der Zeit? Nichts. Also, wirklich nichts. Er dreht keine Runden, und schaut nach, ob die South Bridge mit ihrer Arbeit fertig ist, indem er beispielsweise immer wieder einen bestimmten I/O-Port abfragt, er schreibt auch nicht selbst die Bytes irgendwie auf den USB-Stick. Der Prozessor tut einfach nichts, sein Thread (der UI-Thread) steht und wartet, bis er nach getaner Arbeit quasi wieder von der South Bridge zum Leben erweckt wird. Ganz schön dumm, oder? Was wir hier tagtäglich machen, ist, was ich nicht machen würde, wenn meine Frau an meiner Stelle mit unserem Hund spazieren geht: hinter der Tür stehen und wie festgefroren warten, bis sie zurückkehrt. Was für eine Zeitverschwendung das wäre. Und was für eine Zeitverschwendung es tatsächlich ist, was wir tagtäglich beim Programmieren praktizieren, denn im übertragenen Sinne machen wir nichts anderes. Dabei musste das seit der Version 1.0 des .NET Frameworks gar nicht sein. Wie es viel, viel besser ginge, zeigt Ihnen ein weiterer kleiner Clip (Abb. 3).

Abb. 3: http://youtu.be/u0Rz3tEjp-U

Cool, nicht? Zumindest was das Ergebnis anbelangt. Denn wie Sie sehen konnten, läuft der Zähler nun weiter und bis auf einen ganz kurzen Moment bleibt das Programm reaktionsfreudig. Weniger cool allerdings, was die Lesbarkeit des Codes anbelangt, denn durch den Callback, den wir mit BeginWrite eingefügt haben, können wir nicht mehr auf den ersten Blick sehen, wie der Verlauf des Programms ist. Wenn wir ein paar, oder – dann später in umfangreicheren Programmen – vielleicht sogar zig solcher asynchroner BeginIrgendwas-ses im Programm platziert haben, ergibt das ein richtig schönes Spaghettiprogramm – und dieses Mal kann keiner der Sprache BASIC die Schuld daran geben, denn in C# sähe es exakt genauso aus, nur mit geschweiften Klammern. Es wäre doch schön, wenn es eine Möglichkeit gebe, zwar mit asynchronen Callbacks zu arbeiten, den Code aber dennoch in einer Struktur zu formulieren, die unserem bisher gewohnten Ablaufempfinden von Programmcode entsprechen würde. Und das wollen wir im nächsten Schritt versuchen, durch eine kleine Umgestaltung des Codes. Werfen Sie dazu doch einmal einen Blick auf Listing 1. Hier sehen Sie, dass wir die ursprünglichen zwei Methoden (die mit BeginWrite und dem Callback) in einer Methode zusammengefasst haben, und mit einer ganz simplen State Machine, die durch die Variable IASync gesteuert wird, entscheiden, wann wir den Callback verarbeiten und wann wir den Schreibprozess in Gang bringen. Bei der Callback-Behandlungsmethode handelt es sich in diesem Fall also um dieselbe Methode, die den Callback indirekt durch BeginWrite losgetreten hat. Wenn wir die Methode aufrufen, um die Operation des Speicherstickschreibens mit BeginWrite in Gang zu setzen, geben wir als Callback dieselbe Methode an. Das Schreiben des Speichers wird nun begonnen, und nach dem BeginWrite springt die Programmsteuerung an das Ende der Methode, und – das ist wichtig für die anhaltende Reaktionsfreudigkeit des Programms – kann dann die Windows-Warteschleife weiter ausführen. Aus Programmablaufsicht „hängt“ das Programm dann quasi zwischen dem BeginWrite und den folgenden Befehlen, aber das ist nur konzeptionell so. Inhaltlich wartet das Programm an der Stelle nicht, denn es dreht ja seine Schleifen in der Message Queue von Windows. Konzeptionell könnte man aber sagen, das Programm wartet darauf, dass der Callback vom Schreiben zurückkehrt. Und wenn den Stick inzwischen keiner rausgezogen hat, dann kommt dieser Callback auch und kehrt in dieselbe Methode zurück. Jetzt allerdings hat die Variable iAsyncState einen Inhalt, und deswegen wird nun nicht der erste, sondern der zweite state-dependent (zustandsabhängige) Code der Methode ausgeführt (deswegen die Bezeichnung „State Machine“ für solche Konstrukte).

Public Sub WriteFileAsyncAllInOneMethod(asyncResult As IAsyncResult)
Dim fs As FileStream
If asyncResult IsNot Nothing Then GoTo CompleteAsyncFileOperation

'Wir legen n Bytes an, die geschrieben warden sollen.
Dim byteArr() = GetRandomBytes(NO_OF_BYTES_TO_WRITE)

fs = New FileStream(USB_MEMORY_DRIVE_AND_PATH &
                   "testfile.dat", FileMode.Create)
Dim result = fs.BeginWrite(byteArr, 0, byteArr.Length,
                           AddressOf WriteFileAsyncAllInOneMethod, fs)
GoTo endofproc

'An dieser Stelle "idelt" der UI-Thread, bis die Daten geschrieben wurden.

CompleteAsyncFileOperation:
fs = DirectCast(asyncResult.AsyncState, FileStream)
fs.EndWrite(asyncResult)
fs.Flush()
fs.Close()

endofproc:
End Sub

Halt, Stop, ein Problem gibt es noch zu bewältigen. Wenn Sie genau aufgepasst haben, dann finden Sie noch einen kleinen Fehler im zweiten Beispielprogramm. Nichts Dramatisches, aber es reicht, dass wir das Programm nur ein einziges Mal verwenden können und es dann neu starten müssen. Das Problem: Das Programm macht die Schaltfläche, die dazu führt, dass der Schreibvorgang gestartet wird, nach dem Mausklick unanwählbar – enabled wird auf false gestellt. Es stellt die Schaltfläche nach dem Schreibvorgang aber auch nicht wieder auf enabled. Man könnte meinen, „Hey, das ist ein Einzeiler, wir sind gleich durch mit der Geschichte. „Sorry, sind wir nicht.“ Warum, zeigt der Clip, der sich hinter Abbildung 4 verbirgt.

Abb. 4: http://youtu.be/6hsWAzase8o

So langsam merkt man, warum sich asynchrone Programmierung, die ja seit dem .NET Framework 1.0 prinzipiell möglich war, in „normalen“ Anwendungen nicht durchgesetzt hat, finden Sie nicht auch? Der Aufwand war vielen einfach zu groß, vielfach lag es auch an der Unkenntnis des Zusammenspiels, das Ihnen die einzelnen Clips hoffentlich ein wenig näher bringen konnte. Aber mit das größte Problem wird die Unleserlichkeit der Programme gewesen sein, die sich durch die vielen Callbacks und die Delegierung der Rückrufthreads auf den UI-Thread ergab. Klar war, dass diese Art der Entwicklung sehr viel einfacher werden musste. Und so entstand eine neue Vorgehensweise.

[ header = Seite 2: Async und Await ]

Async und Await

Schön wäre es doch, wenn man – eigentlich ganz ähnlich, wie wir es im Beispiel bereits gemacht haben – den Aufruf einer asynchronen Methode und ihren Rückruf mit einem Schlüsselwort vereinen könnte, und der Compiler die gesamte notwendige Verdrahtung, also das Einrichten der State Machine, selbst durchführen würde. Ein Schlüsselwort wie Await (quasi: abwarten, bis der Rückruf da ist und so lange in der Warteschleife die Nachrichten abarbeiten) musste her. Doch, Problem: Einfach ein neues Schlüsselwort in eine Sprache wie Visual Basic oder C# einzubauen, birgt natürlich viele Risiken. Was wenn vorhandene Programme bereits await als Variable oder Methode genutzt hätten?

Dann ließen diese Programme sich überhaupt nicht mehr kompilieren – das war also keine Option. Man hätte mit zwei Schlüsselwörtern arbeiten können, beispielsweise wait for – eine solche Konstellation hätte es in keinem vorhandenen Quelltext geben können und deswegen wäre es diese Konstellation auch fast geworden. Aber das Managed-Language-Team ging einen anderen Weg. Die Überlegung: Nur wenn man die Methodensignatur umbaut, darf man innerhalb der Methode auch das neue Schlüsselwort verwenden. Und so führte das Team in den Sprachen Visual Basic und C# nicht nur das eigentliche Schlüsselwort Await ein, sondern auch eines namens Async, das innerhalb des Methodenrumpfes platziert werden muss. Und was genau macht Async? Eigentlich nicht viel. Außer das Schlüsselwort Await innerhalb der Methode zuzulassen. Und eine weitere Kleinigkeit, zu der ich noch komme. Was Async nicht macht: Async erstellt keinen neuen Thread, keine Task (auch dazu später mehr), sondern lässt einfach nur Await innerhalb der Methode zu.

Wichtig zu wissen: Wann immer wir selbst eine Methode „awaiten“, muss diese Methode, in der wir das Await anwenden, einen Task zurückgeben (der dann nämlich wiederum selber „awaitet“ werden kann – das ist schon das ganze Geheimnis, warum das so sein muss). Bedeutet: Aus einer synchron aufzurufenden Methode ohne Rückgabewert (void in C#, Sub in Visual Basic) wird eine Funktion in VB bzw. eine Methode mit Rückgabewert in C#, die Task zurückliefert. Eine Funktion bzw. Methode, die ein Funktionsergebnis vom Typ t zurücklieferte, wird zu einer Methode mit dem Rückgabetypen Task(Of t) (bzw. Task in C#). Dementsprechend ändert sich also das Verhalten von Return in mit async gekennzeichneten Methoden, die Task oder Task(Of t) zurückgeben – das ist die zweite Funktion, die dem Async-Schlüsselwort zukommt. Ausnahmen hiervon sind lediglich Ereignismethoden, die eine Kette von asynchronen Aufrufen mit Await initiieren oder ereignisgleiche Methoden, wie etwa Onxxx-Methoden, die Ereignisse auslösen.

So und mit diesem Wissen schauen wir uns jetzt an, wie einfach das asynchrone Speichern unserer 100 Megabyte mit Async und Await vonstattengeht. Im Programmcode (Listing 2) finden Sie die Methode hinter der dritten Schaltfläche.

'Das Async-Schlüsselwort dient NUR dazu, Await zu erlauben!
'(Und um das Rückgabe-Handling zu ändern.
Public Async Function WriteFileCommandProcAsync() As Task

EnsureFolderExist()

Dim fs As FileStream

Dim byteArr = GetRandomBytes(NO_OF_BYTES_TO_WRITE)

'Bis hierher bleibt alles normal.
fs = New FileStream(USB_MEMORY_DRIVE_AND_PATH & "testfile.dat", FileMode,Create)

'Und so wird awaited:
Await fs.WriteAsync(byteArr, 0, byteArr.Count)
Await fs.FlushAsync()

fs.Close()
End Function

Sie sehen: Obwohl es sich um eine Funktion handelt, fehlt hier ein Return, aber der Compiler meckert dennoch nicht. Die Task der Funktion wird quasi vom Await behandelt, übrig bleibt nichts. Würde unsere asynchrone Funktion tatsächlich einen Wert, beispielsweise vom Typ Integer, zurückliefern, würden wir sie als Funktion vom Typ Task(Of Integer) definieren, und dann bräuchten wir ein Return, und das würde dann einen Wert vom Typ Integer zurückliefern – aber: keinen Task(Of Integer), denn der Task wäre sozusagen bereits vom Await verarbeitet worden. Das ist gerade am Anfang ein wenig verwirrend, aber man gewöhnt sich schnell an diese Vorgehensweise. Fassen wir zusammen, was es braucht, um Await anzuwenden:

  1. Sie stellen sicher, dass es eine entsprechende asynchron aufrufbare Methode gibt. Diese gibt es in der Regel dann, wenn sie auf die Zeichenfolge „Async“ endet. Aus Write oder BeginWrite wird also WriteAsync. Aus Flush wird FlushAsync.
  2. Methoden, die Await verwenden, müssen mit Async im Methodenrumpf gekennzeichnet sein. Wichtig: Bei überschriebenen Methoden ist es kein Problem, wenn die Methode der Basisklasse nicht mit Async gekennzeichnet war, denn Async hat keine Funktion, außer dass es dem Compiler beibringt, Await zuzulassen und den Rückgabetyp von Return innerhalb der Async-Methode von Task(Of t) in t zu ändern.
  3. Wenn Sie eigene „awaitbare“ Methoden formulieren, sollten diese deswegen unbedingt eine Task zurückliefern, wenn sie Sub (void) Character haben, ansonsten Task(Of t) (Task in C#).
  4. In der Standardeinstellung brauchen Sie alle nach dem Await folgenden Methoden nicht wie im „manuellen Fall“ auf den UI-Thread zurück delegieren – der Compiler schreibt den entsprechenden Code für Sie automatisch.
  5. Eine asynchrone Funktion, die als Task definiert ist, hat Sub (void) Character. Sie benötigt kein Return. Soll die Funktion einen Rückgabewert vom Typ t zurückliefern, wird sie als Task(Of t) definiert.

Das war es für den ersten Teil. Sie kennen jetzt den Unterschied zwischen CPU- und I/O-lastigem Code, wissen, was Threads sind und was man über Steuerelemente und dem UI-Thread wissen muss, und Sie haben gelernt, wie Sie Async und Await anwenden. Sie sind jetzt bereit, superflüssige Anwendungen für alle möglichen Plattformen, Tablets und Phones zu schreiben. Doch Achtung: Diese Vorgehensweise birgt auch Gefahren. Wie Sie diese vermeiden können, erfahren Sie im nächsten Teil – ein erster Vorgeschmack verbirgt sich hinter dem Code aus Abbildung 5.

Abb. 5: http://youtu.be/JK-3gYIKNN8

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -