VB.NET Tipps: Multithreading

Veröffentlicht: 13. Apr 2006

Von Matthew MacDonald

Auszug aus dem Buch "Microsoft Visual Basic .NET - Programmier-Rezepte - Hunderte von Lösungen und Codebeispielen aus der Praxis"

Das nächste Mal, wenn Sie auf ein Problem in Ihrem Visual Basic .NET-Code stoßen, werfen Sie doch einen Blick in dieses Buch. Hier finden Sie Lösungen und erprobte Techniken.

´Microsoft Visual Basic .NET Programmierrezepte´ ist Ihre Quelle für Hunderte von Visual Basic .NET-Szenarien, die mit Hilfe eines durchgängigen Problem-/Lösungs-Ansatzes erläutert und gelöst werden. Der logische Aufbau des Buchs erlaubt Ihnen, schnell zu den Themen zu gelangen, die Sie interessieren.

Hier finden Sie praktische Beispiele, Code-Schnipsel, erprobte Lösungswege und undokumentierte Vorgehensweisen, die Ihnen helfen, Ihre Arbeit schnell und gut zu erledigen.

Sämtliche Codebeispiele sind von der Microsoft Press-Website downloadbar.

Microsoft Press


Diesen Artikel können Sie dank freundlicher Unterstützung von Microsoft Press auf MSDN Online lesen. Microsoft Press ist ein Partner von MSDN Online.


Zur Partnerübersichtsseite von MSDN Online

Auf dieser Seite

 Das Thread-Objekt mit einem Task benutzen, der Daten benötigt
 Das Thread-Objekt mit einem Task verwenden, der Daten zurückgibt
 Benutzeroberflächencode zum richtigen Thread marshallen
 Einen Thread ordnungsgemäß stoppen
 Eine Thread-Wrapper-Klasse erstellen
 Einen wiederverwendbaren Task-Verarbeiter erstellen
 Einen Thread-Pool verwenden

In der Welt vor Microsoft .NET mussten die Visual Basic-Entwickler viel Aufwand betreiben, um mit Threads arbeiten zu können. Darüber hinaus wurden sie bei diesem Versuch häufig mit beschädigten Daten oder einer abgestürzten IDE (Integrated Development Enivronment) konfrontiert. Wenn Sie mit Microsoft Visual Basic .NET arbeiten, ist der Einsatz von Threads so einfach wie das Erstellen eines Objekts und der Aufruf einer Methode. Ein sicheres und effizientes Multithreading ist jedoch etwas aufwändiger.

HINWEIS: Die Rezepte in diesem Kapitel sind kein Ersatz für eine umfassende Erläuterung des Multithreadings. Wenn Sie zuvor noch keinen Multithreading-Code geschrieben haben, sollten Sie mit einer allgemeinen Einführung beginnen. Das Buch Programming Microsoft Visual Basic .NET Core Reference (Microsoft Press, 2002) und mein eigenes Buch The Book of VB .NET (No Starch Press, 2002) bieten eine solche Einführung.

Entwickler nutzen das Multithreading in ihren Anwendungen, um mehrere Aufgaben gleichzeitig zu bearbeiten (wie ein Server, der mit mehreren Clients gleichzeitig interagieren muss), die Ansprechempfindlichkeit zu verbessern oder die Wartezeit für kurze Aufgaben zu verringern, die andernfalls von anderen arbeitsintensiveren Aufgaben aufgehalten würden. Viele Rezepte in diesem Kapitel benutzen, wie im Folgenden gezeigt, die Methode Delay, um längere Aufgaben zu simulieren:

Private Function Delay(ByVal seconds As Integer) As Integer

    Dim StartTime As DateTime = DateTime.Now
    Do
        ’ Nichts tun.
    Loop While DateTime.Now.Subtract(StartTime).TotalSeconds < seconds
    Return seconds

End Function

Diese Funktion wartet einfach, bis die angegebene Zeit verstrichen ist, und gibt dann die Anzahl der gewarteten Sekunden zurück. Die freigegebene Methode Thread.Sleep könnte ebenfalls für diese Aufgabe verwendet werden. Ich habe mich jedoch für diesen Ansatz entschieden (anstatt den Thread einfach nur für eine gewisse Zeit anzuhalten), da auf diese Weise die Ausführung von Code simuliert wird. Die Methode gibt die Anzahl der gewarteten Sekunden zurück, damit das jeweilige Rezept ein weiteres wichtiges Konzept demonstrieren kann – das Abrufen von Daten aus einem separaten Thread. Dies ist nicht so einfach wie bei einem synchronen Funktionsaufruf.

HINWEIS: Das .NET Framework enthält im Namespace System.Threading Typen für die Multithreading-Programmierung. Die Rezepte in diesem Kapitel setzen voraus, dass Sie diesen Namespace importiert haben.

Das Thread-Objekt mit einem Task benutzen, der Daten benötigt

Aufgabe
Sie möchten einen Task in einem separaten Thread ausführen, und Sie müssen bestimmte Eingabeparameter zur Verfügung stellen.

Lösung
Erstellen Sie eine benutzerdefinierte threadfähige Klasse mit einer parameterlosen Methode. Die zusätzlichen Informationen werden in Form von Member-Variablen angegeben.

Beschreibung

Wenn Sie ein Thread-Objekt erstellen, müssen Sie einen Delegaten zur Verfügung stellen, der auf eine Methode ohne Parameter verweist. Dies führt zu einem Problem, wenn Sie Informationen an den Thread übergeben müssen. Die einfachste Lösung besteht darin, den in einem Thread auszuführenden Code und die benötigten Informationen mit einer einzelnen Klasse zu ummanteln.

Stellen Sie sich vor, Sie möchten im Hintergrund eine große Informationsmenge in eine Datei schreiben. Sie können diese Logik wie folgt in einer Task-Klasse kapseln:

Public Class FileSaver

    Private _FilePath As String
    Private _FileData() As Byte

    Public ReadOnly Property FilePath() As String
        Get
            Return _FilePath
        End Get
    End Property

    Public ReadOnly Property FileData() As Byte()
        Get
            Return _FileData
        End Get
    End Property

    Public Sub New(ByVal path As String, ByVal dataToWrite() As Byte)
        Me._FilePath = path
        Me._FileData = dataToWrite
    End Sub

    Public Sub Save()
        Dim fs As System.IO.FileStream
        Dim w As System.IO.BinaryWriter
        Try
            fs = New System.IO.FileStream(FilePath, IO.FileMode.OpenOrCreate)
            w = New System.IO.BinaryWriter(fs)
            w.Write(FileData)
        Finally
            ' Dies gewährleistet, dass die Datei auch dann geschlossen wird,
            ' wenn der Thread abgebrochen wird.
            w.Close()
        End Try
    End Sub

End Class

Um nun diese Task-Klasse zu benutzen, erstellen Sie ein neues FileSaver-Objekt, setzen dessen Eigenschaften und starten die Methode Save in einem neuen Thread.

Public Module ThreadTest

    Public Sub Main()
        ' Das Task-Objekt erstellen.
        Dim Data() As Byte = {}
        Dim Saver As New FileSaver("myfile.bin", Data)

        ' Thread erstellen.
        Dim FileSaverThread As New Thread(AddressOf Saver.Save)

        FileSaverThread.Start()

        Console.WriteLine("Zum vorzeitigen Beenden Taste drücken...")
        Console.ReadLine()

 
        ' Threads abbrechen, wenn sie noch nicht beendet sind.
        If (FileSaverThread.ThreadState And ThreadState.Running) = ThreadState.Running Then
            FileSaverThread.Abort()
        End If
    End Sub

End Module

 

Das Thread-Objekt mit einem Task verwenden, der Daten zurückgibt

Aufgabe
Sie möchten einen Task in einem separaten Thread ausführen und die von diesem Task produzierten Daten abrufen.

Lösung
Erstellen Sie eine benutzerdefinierte threadfähige Klasse mit einer parameterlosen Methode und einer Methode oder Eigenschaft, über die die Informationen abgerufen werden können. Sie können zusätzlich ein Ereignis verwenden, um den Aufrufer zu benachrichtigen.

Beschreibung

Wenn Sie ein Thread-Objekt erstellen, müssen Sie einen Delegaten zur Verfügung stellen, der auf eine Methode ohne Rückgabewert verweist. Dies schränkt Ihre Möglichkeiten ein, Informationen vom Thread abzurufen, sobald dessen Arbeit verrichtet ist. Die einfachste Lösung besteht darin, den Code, der in einem Thread ausgeführt werden soll, sowie den Rückgabewert mit einer einzelnen Klasse zu ummanteln. Sie können dieser Klasse Methoden hinzufügen, die es dem Haupt-Thread der Anwendung ermöglichen, den Rückgabewert abzurufen und zu verarbeiten. Sie können außerdem ein benutzerdefiniertes Ereignis hinzufügen, mit dem das im Thread ausgeführte Objekt den Haupt-Thread benachrichtigen kann, sobald es seine Arbeit beendet hat.

Sie können eine Funktion in einem threadfähigen Objekt kapseln, das wie folgt aufgebaut ist:

Public Class WeatherLookup

    ' Die Input-Information.
    Private _CityName As String

    ' Die Output-Information.
    Private _Temperature As Single

    ' Die Task-Verarbeitungsinformation.
    Private _IsCompleted As Boolean = False

    ' Ein Ereignis, dass über das Ende des Tasks informiert.
    Public Event WeatherLookupCompleted(ByVal sender As Object, ByVal e As WeatherLookupCompletedEventArgs)

 
    Public ReadOnly Property CityName() As String
        Get
            Return _CityName
        End Get
    End Property

    Public Sub New(ByVal cityQuery As String)
        Me._CityName = cityQuery
    End Sub

    ' Diese Methode wird im neuen Thread ausgeführt.
    Public Sub Lookup()
        ' Dieser Code würde normalerweise die Datenbank nach den verlangten Informationen
        ' abfragen. Stattdessen definieren wir eine Verzögerung und geben eine zufällige
        ' Temperatur zurück.
        Dim Rand As New Random()
        Delay(Rand.Next(5, 10))
        _Temperature = Rand.Next(30, 100)
        _IsCompleted = True
        RaiseEvent WeatherLookupCompleted(Me, New WeatherLookupCompletedEventArgs(_CityName, _Temperature))
    End Sub

    ' Diese Methode wird vom Haupt-Thread aufgerufen, um den Task-Status zu überprüfen.
    Public ReadOnly Property IsCompleted() As Boolean
        Get
            Return _IsCompleted
        End Get
    End Property

    ' Diese Methode wird vom Haupt-Thread aufgerufen, um das Task-Ergebnis abzurufen.
    Public Function GetResult() As Single
        If _IsCompleted Then
            Return _Temperature
        Else
            Throw New InvalidOperationException("Not completed.")
        End If
    End Function

    ' (Die Delay-Funktion ist nicht aufgeführt.)

End Class

HINWEIS: In diesem Beispiel stellt das Task-Objekt ein einfaches IsCompleted-Flag zur Verfügung, mit dem der Haupt-Thread überprüfen kann, ob die Aufgabe beendet ist. Sie könnten natürlich auch eine etwas genauere Fortschrittsanzeige hinzufügen, wie zum Beispiel einen numerischen PercentCompleted-Wert, der den Fortschritt der Aufgabenbearbeitung in Prozent anzeigen würde.

Das Ereignis WeatherLookupCompleted verwendet ein benutzerdefiniertes EventArgs-Objekt, das Informationen über den Namen der Stadt und die abgerufenen Temperaturen enthält:

Public Class WeatherLookupCompletedEventArgs
    Inherits EventArgs

 
    Private _CityName As String
    Private _Temperature As Single
    Public ReadOnly Property CityName() As String
        Get
            Return _CityName
        End Get
    End Property

    Public ReadOnly Property Temperature() As Single
        Get
            Return _Temperature
        End Get
    End Property

    Public Sub New(ByVal cityName As String, ByVal temperature As Single)
        Me._CityName = cityName
        Me._Temperature = temperature
    End Sub

End Class

Die folgende Konsolenanwendung erstellt ein einzelnes Task-Objekt, definiert eine Ereignisbehandlungsroutine und startet den Task in einem gesonderten Thread:

Public Module ThreadTest

    Public Sub Main()
        ' Das Task-Objekt erstellen.
        Dim Lookup As New WeatherLookup("London")
        AddHandler Lookup.WeatherLookupCompleted, AddressOf LookupCompleted

        ' Den Thread erstellen.
        Dim LookupThread As New Thread(AddressOf Lookup.Lookup)
        LookupThread.Start()

        Console.WriteLine("Zum vorzeitigen Beenden Taste drücken...")
        Console.ReadLine()

        ' Threads abbrechen, wenn sie noch nicht beendet sind.
        If (LookupThread.ThreadState And ThreadState.Running) = ThreadState.Running Then
            LookupThread.Abort()
        End If
    End Sub

    Private Sub LookupCompleted(ByVal sender As Object, ByVal e As WeatherLookupCompletedEventArgs)
        Console.WriteLine(e.CityName & " ist: " & e.Temperature)
    End Sub

End Module

Beachten Sie, dass der Ereignis-Handler LookupCompleted im Thread der Temperaturermittlung und nicht im Haupt-Thread der Anwendung ausgeführt wird. Wenn Sie somit in dieser Prozedur eine gemeinsam genutzte Ressource benötigen, müssen Sie mit Sperrcode arbeiten.

Auch bei dieser Vorgehensweise ist der Haupt-Thread für die Erstellung, Überwachung und Verwaltung aller von ihm erstellten Threads verantwortlich. Es ist möglich und oft auch sinnvoll, diese Verantwortung an die jeweilige Task-Klasse zu delegieren. Ein beliebtes Verfahren ist das Erstellen einer Task-Klasse, die als Thread-Wrapper agiert. In “Eine Thread-Wrapper-Klasse erstellen” wird diese Technik gezeigt.

 

Benutzeroberflächencode zum richtigen Thread marshallen

Aufgabe
Sie möchten von einem anderen Thread aus ein Benutzeroberflächenelement eines Fensters aktualisieren.

Lösung
Fügen Sie Ihre Aktualisierungslogik in eine separate Unterroutine ein, und benutzen Sie die Methode Control.Invoke, um diesen Code zum Benutzeroberflächen-Thread zu marshallen.

Beschreibung

Windows-Steuerelemente sind nicht threadsicher. Dies bedeutet, dass es nicht sicher ist, ein Benutzeroberflächen-Steuerelement von einem anderen Thread aus zu aktualisieren als dem, in dem das Element erstellt wurde. Es ist möglich, ohne Schwierigkeiten mit Code zu arbeiten, der diese Warnung ignoriert, nur um nach der Inbetriebnahme in der Praxis feststellen zu müssen, dass derselbe Code zu Problemen führt.

Dieses Problem ist nicht nur auf Code beschränkt, der in einem benutzerdefinierten Thread-Objekt ausgeführt wird. Es betrifft auch Code, der auf einen Rückruf oder ein Ereignis eines Thread-Objekts reagiert. Der Grund hierfür besteht darin, dass der Rückruf oder Ereignisbehandlungscode in dem Arbeits-Thread ausgeführt wird, der den Rückruf oder das Ereignis ausgelöst hat. Sie können dieses Problem glücklicherweise mithilfe der Methode Invoke lösen, die von allen .NET-Steuerelementen angeboten wird. Diese Methode erwartet einen MethodInvoker-Delegaten, der auf eine Methode ohne Parameter und Rückgabewert verweist. Der Code in dieser Methode wird in dem Benutzeroberflächen-Thread ausgeführt.

' UpdateMethod() enthält den Code, der ein Windows-Steuerelement modifiziert.
Dim UpdateDelegate As New MethodInvoker(AddressOf UpdateMethod)

' UpdateMethod() wird nun auf dem Benutzeroberflächen-Thread aufgerufen.
ControlToUpdate.Invoke(UpdateDelegate)

Dies scheint eine sehr einfache Lösung zu sein, was jedoch nicht immer der Fall ist. Diese Technik funktioniert gut, wenn die Aktualisierungslogik nicht auf Variablen zugreifen muss. Doch was geschieht, wenn Sie ein Steuerelement basierend auf den Inhalten einer Variablen aktualisieren müssen? Sie können den Delegaten MethodInvoker nicht benutzen, um auf eine Prozedur zu verweisen, die Argumente erwartet. Sie müssen somit ein benutzerdefiniertes Wrapper-Objekt erstellen.

Eine Möglichkeit bietet die im folgenden Code gezeigte benutzerdefinierte Klasse ControlTextUpdater. Sie referenziert ein einzelnes Steuerelement und stellt Methoden wie AddText und ReplaceText zur Verfügung. Wenn diese Methoden aufgerufen werden, speichern sie den neuen Text in einer Klassen-Member-Variablen und marshallen den Aufruf unter Verwendung der Methode Invoke zum Benutzeroberflächen-Thread.

Public Class ControlTextUpdater

    ' Der Verweis wird als generisches Steuerelement gespeichert, so dass
    ' diese Hilfsklasse in anderen Szenarien wiederverwendet werden kann.
    Private _ControlToUpdate As Control
    Public ReadOnly Property ControlToUpdate() As Control
        Get
            Return _ControlToUpdate
        End Get
    End Property
    Public Sub New(ByVal controlToUpdate As Control)
        Me._ControlToUpdate = controlToUpdate
    End Sub
    ' Speichert den hinzuzufügenden Text.
    Private _NewText As String
    Public Sub AddText(ByVal newText As String)
        SyncLock Me
            Me._NewText = newText
            ControlToUpdate.Invoke(New MethodInvoker(AddressOf ThreadSafeAddText))
        End SyncLock
    End Sub
    Private Sub ThreadSafeAddText()
        Me.ControlToUpdate.Text &= _NewText
    End Sub
    Public Sub ReplaceText(ByVal newText As String)
        SyncLock Me
            Me._NewText = newText
            ControlToUpdate.Invoke(New MethodInvoker(AddressOf ThreadSafeReplaceText))
        End SyncLock
    End Sub
    Private Sub ThreadSafeReplaceText()
        Me.ControlToUpdate.Text = _NewText
    End Sub
End Class

Die Klasse sperrt sich aus Sicherheitsgründen selbst, bevor die Textaktualisierung erfolgt. Wenn andernfalls ein Aufrufer neuen Text zur Verfügung stellen würde, bevor der vorherige Text gespeichert worden wäre, könnte die Aktualisierung fehlschlagen.

Um zu zeigen, wie Sie eine solche Klasse in einer Windows-Anwendung verwenden können, lernen Sie nun ein etwas fortgeschritteneres Beispiel kennen.

Die in Abbildung 1 dargestellte Anwendung stellt ein einzelnes Formular zur Verfügung, mit dem der Benutzer mehrere Tasks gleichzeitig anfordern kann. Jede Anforderung wird in einem separaten Thread bearbeitet, und die Ergebnisse werden threadsicher in ein Textfeld geschrieben.

Ein Frontend für ein Multithreading-Task-Verarbeiter
Abbildung 1: Ein Frontend für ein Multithreading-Task-Verarbeiter

Wir werden uns hier lediglich mit den wichtigsten Elementen beschäftigen. Der Code zur Temperaturermittlung wurde ein wenig modifiziert. Jeder Temperaturermittlungs-Task verfügt nun über einen automatisch zugewiesenen GUID. Auf diese Weise kann jeder Task eindeutig identifiziert werden.

Public Class WeatherLookup

    ' Ein eindeutiger Bezeichner für diesen Task.
    Private _Guid = Guid.NewGuid()
    Public ReadOnly Property Guid() As Guid
        Get
            Return _Guid
        End Get
    End Property
' (Die anderen Eigenschaften und Methoden sind nicht aufgeführt.)

Der GUID wird ebenfalls WeatherLookupCompletedEventArgs hinzugefügt, damit der Client weiß, welcher Task beendet ist, wenn das Ereignis ausgelöst wird.

Das Formular speichert eine Hashtable-Auflistung, die alle Threads aufführt, die gegenwärtig in Bearbeitung sind.

Private TaskThreads As New Hashtable()

Diese Auflistung erlaubt es der Anwendung, die Anzahl der Tasks zu ermitteln, die gegenwärtig in Bearbeitung sind. Ein Timer läuft ohne Unterbrechung und aktualisiert ein Feld in der Statusleiste mit diesen Informationen:

Private Sub tmrRefreshStatus_Tick(ByVal sender As System.Object, _
  ByVal e As System.EventArgs) Handles tmrRefreshStatus.Tick
    pnlStatus.Text = TaskThreads.Count.ToString() & " Task(s) in Verarbeitung."
End Sub

Der Einsatz einer Hashtable-Auflistung ermöglicht es der Anwendung ebenfalls, alle Tasks abzubrechen, wenn die Anwendung beendet wird:

Private Sub Form1_Closing(ByVal sender As Object, _
  ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
    ' Alle noch nicht beendeten Threads abbrechen.
    Dim TaskThread As Thread
    Dim Item As DictionaryEntry
    For Each Item In TaskThreads
        TaskThread = Item.Value
        If (TaskThread.ThreadState And ThreadState.Running) = ThreadState.Running Then
            TaskThread.Abort()
        End If
    Next

End Sub

Wenn auf die Schaltfläche Anzeigen geklickt wird, werden ein neues Task-Objekt und ein neuer Thread erstellt. Der Thread wird in der Auflistung TaskThreads gespeichert. Als Indexzahl wird der GUID des Threads verwendet. Das Task-Objekt wird nicht gespeichert, obwohl dies möglich wäre.

Private Sub cmdLookup_Click(ByVal sender As System.Object, _
  ByVal e As System.EventArgs) Handles cmdLookup.Click
    ' Das Task-Objekt erstellen.
    Dim Lookup As New WeatherLookup(txtCity.Text)
    AddHandler Lookup.WeatherLookupCompleted, AddressOf LookupCompleted
    ' Den Thread erstellen.
    Dim LookupThread As New Thread(AddressOf Lookup.Lookup)
    LookupThread.Start()
    TaskThreads.Add(Lookup.Guid.ToString(), LookupThread)
End Sub

Sobald die Temperaturermittlung abgeschlossen ist, wird das Ereignis WeatherLookupCompleted ausgelöst. Jetzt wird ein neuer Updater erstellt, um den abgerufenen Text sicher im Textfeld anzuordnen. Der entsprechende Thread wird außerdem aus der Auflistung TaskThreads entfernt. Beachten Sie, dass für diesen Schritt Synchronisierungscode erforderlich ist, da ein anderer Thread (zum Beispiel der Haupt-Thread, wenn der Timer ausgelöst wird oder der Benutzer eine neue Temperaturermittlung initiiert) auf die Auflistung TaskThreads zugreifen könnte.

Private Sub LookupCompleted(ByVal sender As Object, _
    ByVal e As WeatherLookupCompletedEventArgs)

    ' Das Aktualisierungsobjekt erstellen.
    Dim Updater As New ControlTextUpdater(txtResults)
    ' Eine thread-sichere Aktualisierung durchführen.
    Updater.AddText(e.CityName & " ist: " & e.Temperature.ToString() & vbNewLine)

 
    ' Das Task-Objekt löschen.
    SyncLock TaskThreads
        TaskThreads.Remove(e.TaskGuid.ToString())
    End SyncLock
End Sub

WARNUNG: Wenn ein Benutzer beliebig viele Threads erstellen kann, ist das Chaos vorprogrammiert. Je mehr Threads generiert werden, desto langsamer läuft das System. Die Lösung besteht darin, die Anzahl der Tasks in der Warteschlange zu überprüfen und die Schaltfläche Anzeigen zu sperren, sobald ein gewisser Grenzwert erreicht ist.

 

Einen Thread ordnungsgemäß stoppen

Aufgabe
Sie möchten einen Thread stoppen. Die aktuelle Aufgabe soll vorzeitig aber ordnungsgemäß beendet werden.

Lösung
Erstellen Sie ein boolesches StopRequested-Flag, das der im Thread ausgeführte Code regelmäßig auslesen kann.

Beschreibung

Wenn Sie die Methode Abort benutzen, um einen Thread abzubrechen, wird die Ausnahme ThreadAbortException für den Code ausgelöst, der gegenwärtig ausgeführt wird. Der Thread kann diese Ausnahme in einem Catch- oder Finally-Block behandeln, doch sie kann nicht verhindert werden. Die Ausnahme wird sogar nach ihrer Behandlung noch einmal ausgelöst, nachdem der Code im Catch- oder Finally-Block ausgeführt wurde. Dies ist solange möglich, bis der Thread beendet ist. Die Methode Abort ist nicht immer geeignet. Eine etwas weniger Unruhe stiftende Vorgehensweise besteht darin, einen besonnenen Thread zu erstellen, der regelmäßig überprüft, ob eine Stopp-Anforderung vorliegt. Nachfolgend ist beispielsweise ein benutzerdefiniertes Thread-Objekt aufgeführt, dass zehn Minuten lang eine Schleife ausführt, die vorzeitig beendet wird, wenn eine Stopp-Anforderung eingeht.

Public Class Worker
    Private _StopRequested As Boolean = False

    Public Sub RequestStop()
        _StopRequested = True
    End Sub

    Public Sub DoWork()
        Dim StartTime As DateTime = DateTime.Now
        Do
            If _StopRequested Then
                Exit Do
            End If
        Loop Until DateTime.Now.Subtract(StartTime).TotalMinutes > 9
    End Sub
End Class

Mit der folgenden Konsolenanwendung können Sie die Klasse Worker testen. Drei Schritte sind notwendig:

  • Der Stopp wird angefordert.

  • Der Code wartet maximal 30 Sekunden darauf, dass der Thread die Arbeit einstellt.

  • Wenn der Thread immer noch ausgeführt wird, benutzt der Code die Methode Abort.

Public Module ThreadTest

    Public Sub Main()

        ' Das Task-Objekt erstellen.
        Dim Worker As New Worker()

        ' Den Thread erstellen.
        Dim WorkerThread As New Thread(AddressOf Worker.DoWork)
        WorkerThread.Start()

        Console.WriteLine("Drücken Sie eine Taste, um einen Stopp anzufordern...")
        Console.ReadLine()

        Worker.RequestStop()

        ' Auf den Stopp des Threads (oder auf ein Timeout nach 30 Sekunden) warten.
        WorkerThread.Join(TimeSpan.FromSeconds(30))

        If WorkerThread.ThreadState = ThreadState.Running Then
            Console.WriteLine("Ein Abbruch war erforderlich.")
            WorkerThread.Abort()
        End If

        Console.WriteLine("Thread wurde gestoppt.")
        Console.ReadLine()
    End Sub

End Module

Das Starten und Stoppen eines Threads ist eine oft erforderliche Interaktion zwischen dem Haupt-Thread und dem Arbeits-Thread Ihrer Anwendung. Sie können jedoch auch zusätzliche Methoden hinzufügen, um andere Anweisungen an Ihren Thread-Code zu senden.

 

Eine Thread-Wrapper-Klasse erstellen

Aufgabe
Sie möchten den Thread-Verwaltungscode aus Ihrem Haupt-Thread entfernen. Stattdessen sollen die Task-Objekte ihre Threads selbst verwalten.

Lösung
Erstellen Sie eine Thread-Wrapper-Klasse, die einen Verweis auf den Thread speichert und die spezifische Task-Logik kapselt.

Beschreibung

Eine übliche Vorgehensweise beim Multithreading ist die Erstellung eines Thread-Wrappers. Dieser Wrapper bietet die typischen Methoden an, die Sie in einem Thread erwarten, wie zum Beispiel Start und Stop. Er enthält außerdem den spezifischen Task-Code und stellt Eigenschaften für die erforderlichen Eingabewerte und die berechneten Werte zur Verfügung.

Nachfolgend ist eine abstrakte Basisklasse aufgeführt, die die grundlegende Struktur eines ordnungsgemäßen Thread-Wrappers definiert:

Public MustInherit Class ThreadWrapperBase

    ' Dies ist der Thread, in dem der Task ausgeführt wird.
    Public ReadOnly Thread As System.Threading.Thread

    Public Sub New()
        ' Thread erstellen.
        Me.Thread = New System.Threading.Thread(AddressOf Me.StartTask)
    End Sub

    ' Den Task im Arbeits-Thread starten.
    Public Overridable Sub Start()
        Me.Thread.Start()
    End Sub

    ' Den Task durch einen Abbruch des Threads stoppen.
    ' Sie können diese Methode überschreiben, um stattdessen
    ' eine ordentliche Stopp-Anforderung zu verwenden.
    Public Overridable Sub [Stop]()
        Me.Thread.Abort()
    End Sub

    ' Den Status des Tasks überwachen.
    Private _IsCompleted As Boolean

    Public ReadOnly Property IsCompleted() As Boolean
        Get
            Return _IsCompleted
        End Get
    End Property

    Private Sub StartTask()
        _IsCompleted = False
        DoTask()
        _IsCompleted = True
    End Sub

    ' Diese Methode enthält den Code, der die eigentliche Aufgabe ausführt.
    Protected MustOverride Sub DoTask()

End Class

Und hier eine sehr einfache Task-Klasse, die von der abstrakten Basisklasse abgeleitet ist:

Public Class Worker
    Inherits ThreadWrapperBase

    ' (Sie können an dieser Stelle Eigenschaften für Input- und (berechnete)
    ' Output-Werte hinzufügen.
    ' (Sie können ebenfalls ein WorkerCompleted-Ereignis definieren.)

    Protected Overrides Sub DoTask()
        Dim StartTime As DateTime = DateTime.Now
        Do
        Loop Until DateTime.Now.Subtract(StartTime).TotalMinutes > 4
    End Sub

End Class

Der Thread-Wrapper verbessert die Kapselung und vereinfacht die Anwendungsprogrammierung, da nur das Worker-Objekt (und nicht sowohl das Worker-Objekt als auch das Thread-Objekt ) überwacht werden muss. Der folgende Code testet die beiden Klassen:

Public Module ThreadTest
    Public Sub Main()
        ' Das Task-Objekt erstellen.
        Dim Worker As New Worker()

        ' Den Task in seinem internen Thread starten.
        Worker.Start()

        Console.WriteLine("Zum Beenden Taste drücken...")
        Console.ReadLine()

        ' Task abbrechen, wenn erforderlich.
        If Not Worker.IsCompleted Then
            Console.WriteLine("Thread wird noch ausgeführt.")
            Worker.Stop()
            Console.WriteLine("Thread wurde abgebrochen.")
        End If

        Console.ReadLine()
    End Sub

End Module

 

Einen wiederverwendbaren Task-Verarbeiter erstellen

Aufgabe
Sie benötigen einen Thread, der fortlaufend Warteschlangen-Tasks verarbeitet.

Lösung
Erstellen Sie eine threadfähige Klasse, die Tasks in einer ständig überwachten Warteschlange speichert.

Beschreibung

Ein Task-Verarbeiter ist eine Technik, die häufig für verteilte Anwendungen benutzt wird. Dabei handelt es sich um einen Thread, der angeforderte Tasks nach dem FIFO-Prinzip (first in, first out) verarbeitet. Dieser Thread existiert genauso lange wie die Anwendung und verwendet eine interne Auflistung (typischerweise eine Warteschlange), um die angeforderten Tasks solange zu speichern, bis er sie verarbeiten kann.

Das Erstellen eines Task-Verarbeiters ist recht einfach. Sie benötigen dazu das Stopp-Schema sowie das Thread-Wrapper-Schema aus „Einen Thread ordnungsgemäß stoppen“ und „Eine Thread-Wrapper-Klasse erstellen“

Der erste Schritt besteht darin, eine Klasse zu definieren, die die Task-Daten kapselt. In unserem Beispiel kann jeder Task einen binären Datenblock enthalten (z. B. ein Bild, das verarbeitet werden muss, oder ein Dokument, das verschlüsselt werden soll), sowie einen GUID, der den Task eindeutig identifiziert.

Public Class Task

    Private _SomeData() As Byte
    Private _Guid As Guid = Guid.NewGuid()

    Public Property SomeData() As Byte()
        Get
            Return _SomeData
        End Get
        Set(ByVal Value() As Byte)
            _SomeData = Value
        End Set
    End Property

    Public ReadOnly Property Guid() As Guid
        Get
            Return _Guid
        End Get
    End Property

End Class

Die Klasse TaskProcessor ist von ThreadWrapperBase abgeleitet. Sie fügt die Methode SubmitTask hinzu, mit der neue Tasks in der Warteschlange angeordnet werden können, sowie die Methode GetIncompleteTaskCount, mit der Sie die Anzahl der ausstehenden Tasks überprüfen können. TaskProcessor überschreibt außerdem die Methode DoTask mit dem Worker-Code. DoTask wird kontinuierlich in einer Schleife ausgeführt (sofern nicht ein Stopp angefordert wird) und nimmt nacheinander die Elemente aus der Warteschlange, um diese zu verarbeiten. Wird kein Element gefunden, pausiert der Thread für fünf Sekunden. Außerdem wird die Methode Stop überschrieben, so dass sie versuchen kann, den Thread mithilfe des Flags _StopRequested ordnungsgemäß zu stoppen, bevor sie die Methode Abort verwendet.

Public Class TaskProcessor
    Inherits ThreadWrapperBase

    Private _Tasks As New Queue()
    Private _StopRequested As Boolean

    Public Sub SubmitTask(ByVal task As Task)
        SyncLock _Tasks
            _Tasks.Enqueue(task)
        End SyncLock
    End Sub

    Public Function GetIncompleteTaskCount() As Integer
        Return _Tasks.Count
    End Function

    Protected Overrides Sub DoTask()
        Do
            If _Tasks.Count > 0 Then
                SyncLock _Tasks
                    ' Den nächsten Task abrufen (in FIFO-Reihenfolge).
                    Dim Task As Task = CType(_Tasks.Dequeue(), Task)
                End SyncLock

                ' (Task hier verarbeiten.)
                Delay(10)
            Else
                ' Keine Tasks hier. Verarbeitung 5 Sekunden aufschieben.
                Thread.Sleep(TimeSpan.FromSeconds(5))
            End If
        Loop Until _StopRequested
    End Sub

    Public Overrides Sub [Stop]()
        ' 10 Sekunden lang einen ordentlichen Stopp anfordern.
        _StopRequested = True
        Me.Thread.Join(TimeSpan.FromSeconds(10))

        ' Die Basisklassenimplementierung aufrufen,
        ' die den Thread gegebenfalls abbricht.
        MyBase.Stop()
    End Sub


    ' (Die Delay-Funktion ist nicht aufgeführt.)

End Class

Die folgende Konsolenanwendung zeigt den Task-Verarbeiter in Aktion. Sie erstellt und übergibt drei Tasks und bricht die Ausführung des Verarbeiters ab, wenn der Benutzer die Taste Enter drückt.

Public Module ThreadTest

    Public Sub Main()

        ' Den Task-Verarbeiter erstellen.
        Dim TaskProcessor As New TaskProcessor

        ' Den Task-Verarbeiter starten.
        TaskProcessor.Start()

        ' Drei Tasks zuweisen.
        TaskProcessor.SubmitTask(New Task())
        TaskProcessor.SubmitTask(New Task())
        TaskProcessor.SubmitTask(New Task())

        Console.WriteLine(TaskProcessor.GetIncompleteTaskCount().ToString & " Tasks werden ausgeführt.")

        Console.WriteLine("Drücken Sie zum Stoppen eine Taste...")
        Console.ReadLine()

        Console.WriteLine(TaskProcessor.GetIncompleteTaskCount().ToString & " Tasks werden noch ausgeführt.")
        TaskProcessor.Stop()
    End Sub

End Module

 

Einen Thread-Pool verwenden

Aufgabe
Sie möchten eine unbegrenzte Anzahl von Threads erstellen, ohne eine Einbuße der Performance hinnehmen zu müssen.

Lösung
Benutzen Sie die Klasse ThreadPool, um einer festen Anzahl wiederverwendbarer Threads eine große Anzahl von Tasks zuzuweisen.

Beschreibung

Mit der Klasse System.Threading.ThreadPool können Sie Code unter Verwendung eines Thread-Pools ausführen, der von der CLR (Common Language Runtime) zur Verfügung gestellt wird. Der Einsatz dieses Pools vereinfacht Ihren Code und kann die Performance verbessern. Dies gilt besonders dann, wenn Sie viele kurzlebige Threads verwenden. Die Klasse ThreadPool vermeidet den Aufwand, der mit der ständigen Erstellung und Löschung von Threads einhergeht, indem sie ein Pool mit wiederverwendbaren Threads verwendet. Sie benutzen die Klasse ThreadPool, um einen Task in eine Warteschlange zu stellen. Der Task wird dann im ersten verfügbaren Thread verarbeitet. Die Klasse ThreadPool ist auf 25 Threads pro CPU begrenzt, so dass sie nicht mit der Performance-Einbuße konfrontiert werden können, die bisweilen auftritt, wenn Sie mehr Threads erstellen, als das Betriebssystem gleichzeitig verarbeiten kann.

Die Task-Einteilung ist mit ThreadPool sehr einfach. Zwei Voraussetzungen müssen erfüllt sein. Die Methode, die Sie in die Warteschlange stellen möchten, muss zunächst die Signatur des Delegaten WaitCallback aufweisen. Sie muss somit ein einzelnes Statusobjekt akzeptieren:

Public Sub WaitCallback(ByVal state As Object)
    ' Der Code ist nicht aufgeführt.
End Sub

Das Statusobjekt ermöglicht es Ihnen, der asynchronen Methode Informationen über den Task zu übergeben. Wenn Sie jedoch Ihren Task mit einem benutzerdefinierten Task-Objekt ummanteln, benötigen Sie diese Informationen nicht, da sich in diesem Fall alle Statusdetails in der Task-Klasse selbst befinden.

Wenn Sie eine Methode geschrieben haben, die der verlangten Signatur entspricht, können Sie sie an die freigegebene Methode ThreadPool.QueueUserWorkItem übergeben. (Sie können ebenfalls das optionale Statusobjekt übergeben.) Die Methode QueueUserWorkItem teilt den Task für die asynchrone Ausführung ein. Wenn ein Thread verfügbar ist, wird die Verarbeitung innerhalb kürzester Zeit gestartet.

ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf WaitCallback), _
  Nothing)

Nachfolgend finden Sie eine einfache Worker-Klasse, die mit ThreadPool verwendet werden kann:

Public Class Worker

    Private _IsCompleted As Boolean

    Public ReadOnly Property IsCompleted() As Boolean
        Get
            Return _IsCompleted
        End Get
    End Property

    Public Sub DoWork(ByVal state As Object)
        _IsCompleted = False
        Delay(10)
        _IsCompleted = True
    End Sub

    ' (Die Delay-Funktion ist nicht aufgeführt.)

End Class

Die folgende Konsolenanwendung stellt zwei asynchron auszuführende Worker-Elemente in die Warteschlange und wartet auf das Ende der Verarbeitung dieser Elemente:

Public Module ThreadTest

    Public Sub Main()

        Dim WorkerThreads, CompletionPortThreads As Integer
        ThreadPool.GetMaxThreads(WorkerThreads, CompletionPortThreads)
        Console.WriteLine("Dieser Thread-Pool enthält " & WorkerThreads.ToString() & " Threads.")
        ThreadPool.GetAvailableThreads(WorkerThreads, CompletionPortThreads)
        Console.WriteLine(WorkerThreads.ToString() & " Threads sind gegenwärtig verfügbar.")

        ' Zwei Task-Objekte erstellen und in Warteschlange anordnen.
        Dim WorkerA As New Worker()
        ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf WorkerA.DoWork), Nothing)
        Dim WorkerB As New Worker()
        ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf WorkerB.DoWork), Nothing)

        ' Einige Sekunden auf den Start der Objekte warten.
        Thread.Sleep(TimeSpan.FromSeconds(3))

        ThreadPool.GetAvailableThreads(WorkerThreads, CompletionPortThreads)
        Console.WriteLine(WorkerThreads.ToString() & " Threads sind gegenwärtig verfügbar.")

        Do
        Loop Until WorkerA.IsCompleted And WorkerB.IsCompleted

        Console.WriteLine("Alle Objekte in Warteschlange wurden abgeschlossen.")
        ThreadPool.GetAvailableThreads(WorkerThreads, CompletionPortThreads)
        Console.WriteLine(WorkerThreads.ToString() & " Threads sind gegenwärtig verfügbar.")

        Console.ReadLine()
    End Sub

End Module

Die Ausgabe dieser Anwendung ist nachfolgend aufgeführt:

Dieser Thread-Pool enthält 25 Threads.
25 Threads sind gegenwärtig verfügbar.
23 Threads sind gegenwärtig verfügbar.
Alle Objekte in Warteschlange wurden abgeschlossen.
25 Threads sind gegenwärtig verfügbar.

Der Einsatz der ThreadPool-Klasse von .NET ist leider nicht so leistungsfähig wie ein eigener Thread-Pool. Es gibt keine Möglichkeit, Tasks abzubrechen, nachdem sie dem Thread-Pool übergeben wurden. Sie können auch keine Priorität festlegen, um die Tasks in einer anderen Reihenfolge verarbeiten zu lassen, als der, in der sie entgegen genommen wurden. Es ist auch nicht möglich, Informationen über laufende Tasks von ThreadPool abzurufen – Sie können lediglich die Anzahl der verfügbaren Threads ermitteln.

HINWEIS: Die Klasse ThreadPool ist eine in der gesamten Anwendung gültige Ressource. Wenn Sie somit ThreadPool verwenden, um mehrere verschiedene Tasks auszuführen, müssen sich alle Ihre Worker-Elemente die verfügbaren 25 Threads teilen.