5.4 Asynchrone Methodenaufrufe
Im Kontext dieses Kapitels können Sie eine Methode auf zwei Arten aufrufen:
- synchron: Die Programmausführung fährt erst nach Beendigung der Methode fort.
- asynchron: Die Methode wird gestartet, und die Programmausführung fährt (nebenläufig) fort, ohne auf das Ende der Methode zu warten.
Mit den Methoden der bisherigen Abschnitte dieses Kapitels sind wir bereits in der Lage, alle Arten von asynchronen Aufrufen zu programmieren. Im Wesentlichen lassen wir dazu eine Methode durch einen eigenen Thread starten. Damit sind aber noch nicht alle Probleme gelöst.
In den bisherigen Beispielen dieses Kapitels haben wir zwar mit verschiedenen Threads auf Daten operiert, mussten uns aber selbst darum kümmern, wann die Aktionen abgeschlossen waren. Außerdem hatten alle Methoden keinen Rückgabewert. In diesem Abschnitt zeige ich Ihnen, wie eine Methode eines anderen Threads den Aufrufer über ihren Abschluss informiert und ihm einen Wert zurückgibt.
Hinweis |
Abschnitt 3.9.4, »Asynchrone Aufrufe«, zeigt im Grunde bereits den kompletten Ablauf. Ich greife das Thema bewusst ein zweites Mal auf, da es einerseits thematisch auch zu diesem Kapitel gehört und andererseits, weil ich hoffe, durch die Wiederholung auch die Programmierer zu erreichen, die asynchrone Aufrufe in eigenen Programmen vermeiden, weil sie die entsprechenden Konzepte nicht verinnerlicht haben. |
In diesem Abschnitt verwenden wir als Beispiel Aufträge an das Küchenpersonal in einem Restaurant. Jemand soll den Nachtisch machen, bekommt dafür die Zutaten und kann Bescheid sagen, ob das Dessert gut geworden ist und in welchem Kühlschrank es abkühlt. Die Qualität der Arbeit wird in fünf Stufen modelliert:
- Nachtisch wird gemacht (Aufruf).
- Der Konditor meldet außerdem »fertig« (Rückruf).
- Zusätzlich kennt er den Auftraggeber (Zustand).
- Zusätzlich sagt er, wie lange er gebraucht hat (Rückgabe).
- Zusätzlich sagt er, in welchem Kühlschrank sein Werk abkühlt (ByRef-Parameter).
Um die Beispiele einfach zu halten, sind Synchronisationsprobleme ausgeklammert.
Hinweis |
Asynchrone Aufrufe nutzen den Threadpool (siehe Abschnitt 5.5, »Threadpools«). |
5.4.1 Aufruf
Da letztendlich immer ein Thread die asynchrone Ausführung übernimmt und diesem im Konstruktor ein passendes Delegate übergeben wird, müssen asynchrone Methodenaufrufe über ein Delegate aufgerufen werden. Wie in Abschnitt 3.9.2, »Automatisch generierter Code«, beschrieben, ist ein Delegate nichts anderes als eine Klasse, in die der Compiler automatisch zwei Methoden schreibt, die einen einfachen asynchronen Aufruf erlauben. Mit der ersten starten wir den Aufruf.
BeginInvoke(<p>, rückruf As AsyncCallback, status As Object) As IAsyncResult <p>: alle Methodenparameter in der richtigen Reihenfolge |
Hinweis |
Das eigentlich erforderliche EndInvoke zeigt Abschnitt 5.4.4, »Rückgabe«. |
Das folgende Beispiel ist der einfachste Fall eines asynchronen Aufrufs. Da wir den Wert Nothing für die beiden letzten Parameter verwenden, ist es eine Einwegekommunikation: Die Methode wird aufgerufen und dann »vergessen«.
'...\ Multitasking\Asynchron\Aufruf.vb |
Option Strict On Imports System.Threading Namespace Multitasking Module Aufruf Delegate Sub Nachtisch(ByVal zutaten As String) Sub Machen(ByVal zutaten As String) For i As Integer = 0 To 10 Console.Write("|") : Thread.Sleep(10) Next End Sub Sub Test() Dim del As New Nachtisch(AddressOf Machen) del.BeginInvoke("Schoko", Nothing, Nothing) For i As Integer = 0 To 200 Console.Write("-") : Thread.Sleep(10) Next Console.WriteLine("Arbeit abgeschlossen.") Console.ReadLine() End Sub End Module End Namespace
Eine typische Ausgabe zeigt, wie sich – nach der zum Start der asynchronen Methode Machen notwendigen Zeit – die beiden Threads abwechseln.
-------------------------------------------------------------------------- -------------------------|-|-||-|-|-|-|-|--|-|---------------------------- ----------------------------------------------------------------Arbeit abg eschlossen.
5.4.2 Rückruf
Kommen wir zur nächsten Stufe: Die aufgerufene Methode meldet sich beim Aufrufer, dass sie fertig ist. Dazu muss im AsyncCallback-Parameter eine Methode angegeben werden, die automatisch bei Beendigung der Methode aufgerufen wird. Die Signatur dieser Rückrufmethode ist festgelegt.
Public Delegate Sub AsyncCallback(ar As IAsyncResult) |
In unserem Beispiel schreibt die Rückrufmethode auch in die Konsole. Zur Identifikation werden außerdem Threadnummern ausgegeben.
'...\ Multitasking\Asynchron\Rückruf.vb |
Option Strict On Imports System.Threading Namespace Multitasking Module Rückruf Delegate Sub Bescheid(ByVal zutaten As String) Sub Machen(ByVal zutaten As String) Console.Write("Beginn in Thread {0}", _ Thread.CurrentThread.ManagedThreadId) For i As Integer = 0 To 10 Console.Write("|") : Thread.Sleep(10) Next Thread.Sleep(100) End Sub Sub Fertig(ByVal ar As IAsyncResult) Console.Write("fertig in Thread {0}", _ Thread.CurrentThread.ManagedThreadId) End Sub Sub Test() Dim del As New Bescheid(AddressOf Machen) Console.Write("Hauptthread {0}",Thread.CurrentThread.ManagedThreadId) del.BeginInvoke("Schoko", AddressOf Fertig, Nothing) For i As Integer = 0 To 200 Console.Write("-") : Thread.Sleep(10) Next Console.WriteLine("Arbeit abgeschlossen.") Console.ReadLine() End Sub End Module End Namespace
Eine typische Ausgabe zeigt, wie nach einiger Zeit die Rückmeldung erfolgt. Der Abstand zu den Ausgaben von Machen ist bedingt durch die zusätzliche Sleep-Anweisung an deren Ende. Außerdem sehen Sie, dass die Rückmeldung innerhalb des Threads der aufgerufenen Methode erfolgt, es wird also kein neuer Thread für den Rückruf erstellt.
Hauptthread 8------------------------------------------------------------- ----------------------------------------Beginn in Thread 10|-|-|-|-|-|-|-| -||-|-----------fertig in Thread 10--------------------------------------- -----------------------------------------Arbeit abgeschlossen.
5.4.3 Zustand
Die Methode BeginInvoke hat einen letzten Parameter, mit dem Sie Informationen an die Rückrufmethode durchreichen können. Welche das sind, hängt ausschließlich von der Logik Ihrer Anwendung ab. In unserem Beispiel übergeben wir nur der Einfachheit halber eine Zeichenkette.
'...\ Multitasking\Asynchron\Zustand.vb |
Option Strict On Imports System.Threading Namespace Multitasking Module Zustand Delegate Sub Auftraggeber(ByVal zutaten As String) Sub Machen(ByVal zutaten As String) For i As Integer = 0 To 10 Console.Write("|") : Thread.Sleep(10) Next Thread.Sleep(100) End Sub Sub Fertig(ByVal ar As IAsyncResult) Console.Write("Hallo {0}, bin fertig", ar.AsyncState) End Sub Sub Test() Dim del As New Auftraggeber(AddressOf Machen) del.BeginInvoke("Schoko", AddressOf Fertig, "Paul") For i As Integer = 0 To 200 Console.Write("-") : Thread.Sleep(10) Next Console.WriteLine("Arbeit abgeschlossen.") Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt die an BeginInvoke übergebene Zeichenkette:
-------------------------------------------------------------------------- ---------------------------|-|-|-|-|-|-|-|-|-|-|----------Hallo Paul, bin fertig-------------------------------------------------------------------- ------------Arbeit abgeschlossen.
5.4.4 Rückgabe
Asynchrone Methoden dürfen auch einen Wert zurückgeben. Um an den Wert zu kommen, brauchen Sie das richtige Objekt vom Typ IAsyncResult: Es wird sowohl von BeginInvoke zurückgegeben als auch als Parameter an eine Rückrufmethode übergeben. Sie übergeben das Objekt der Methode EndInvoke des Delegates.
EndInvoke(<nur params mit ByRef>, ar As IAsyncResult) As <Rückgabetyp> |
Hinweis |
Der Aufruf blockiert gegebenenfalls so lange, bis die asynchron aufgerufene Methode fertig ist. EndInvoke darf maximal einmal pro asynchronem Aufruf aufgerufen werden. Der Aufruf von EndInvoke ist erforderlich, auch wenn keine Rückgabe ausgewertet wird (das exakte Warum wird im Internet kontrovers diskutiert). |
Im folgenden Beispiel ist kein Parameter der asynchron aufgerufenen Methode mit ByRef gekennzeichnet, sodass EndInvoke nur mit dem letzten Argument aufgerufen wird. Der Aufruf von EndInvoke gibt den Rückgabewert der asynchron aufgerufenen Methode typrichtig zurück. Das ist einer der Gründe dafür, warum der Compiler den Code automatisch generiert und nicht eine allgemeine »Delegate-Klasse« zur Verfügung gestellt wird. Um die Ausgabe zu kürzen, testen wir in der Schleife des Hauptthreads, ob die asynchrone Methode fertig ist.
'...\ Multitasking\Asynchron\Rückgabe.vb |
Option Strict On Imports System.Threading Namespace Multitasking Module Rückgabe Delegate Function Zeit(ByVal zutaten As String) As Integer Function Machen(ByVal zutaten As String) As Integer For i As Integer = 0 To 10 Console.Write("|") : Thread.Sleep(10) Next Thread.Sleep(100) Return 200 End Function Sub Test() Dim del As New Zeit(AddressOf Machen) Dim ar As IAsyncResult = del.BeginInvoke("Schoko", Nothing, Nothing) For i As Integer = 0 To 200 If ar.IsCompleted Then Exit For Console.Write("-") : Thread.Sleep(10) Next Console.WriteLine("Zeitbedarf: {0} Minuten", del.EndInvoke(ar)) Console.ReadLine() End Sub End Module End Namespace
Der Hauptthread bekommt den richtigen Rückgabewert von EndInvoke.
-------------------------------------------------------------------------- -----------------------|-|-|-|-|-|-|-|-||-|-----------Zeitbedarf: 200 Minu ten
5.4.5 ByRef-Parameter
Das vorige Beispiel muss nur minimal geändert werden, um auch ByRef-Parameter zu erfassen, hier ist das der kühlschrank. An welcher Stelle diese Art Parameter in der Parameterliste stehen, spielt keine Rolle. Wichtig ist, dass EndInvoke nur mit den ByRef-Parametern der asynchron aufgerufenen Methode aufgerufen wird.
'...\ Multitasking\Asynchron\Rückgabe.vb |
Option Strict On Imports System.Threading Namespace Multitasking Module ByRefParameter Delegate Sub Kühlschrank(ByRef kühlschrank As Integer, _ ByVal zutaten As String) Sub Machen(ByRef kühlschrank As Integer, ByVal zutaten As String) For i As Integer = 0 To 10 Console.Write("|") : Thread.Sleep(10) Next Thread.Sleep(100) kühlschrank = 4 End Sub Sub Test() Dim ks As Integer Dim del As New Kühlschrank(AddressOf Machen) Dim ar As IAsyncResult = del.BeginInvoke(ks,"Schoko",Nothing,Nothing) For i As Integer = 0 To 200 If ar.IsCompleted Then Exit For Console.Write("-") : Thread.Sleep(10) Next del.EndInvoke(ks, ar) Console.WriteLine("Kühlschrank {0}", ks) Console.ReadLine() End Sub End Module End Namespace
Nach dem Aufruf von EndInvoke hat der Hauptthread den richtigen Wert.
-------------------------------------------------------------------------- ----------------------------|-|-|-|-|-|-|-|-|-|-|----------Kühlschrank 4
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.