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 |
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.