Multithreading mit BackgroundWorker

    • VB.NET

    Es gibt 12 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      Multithreading mit BackgroundWorker

      Da mir in den letzten Tagen aufgefallen ist, das der Umgang mit BackgroundWorkern am Anfang nicht einfach ist, habe ich mich dazu entschlossen dieses Tutorial zu schreiben.

      Für die, die sich noch nie damit beschäftigt haben: Multithreading kann dazu verwendet werden zeitaufwendige Aufgaben auszuführen und gleichzeitig das GUI weiter verwenden zu können.
      Um es an einem Beispiel zu erklären: Eine Schleife mit 100000 Durchläufen benötigt insgesamt ~20 Sekunden (angenommener Wert). Das bedeutet, das beim Click auf den Button die Form 20 Sekunden lang hängt. Bei Windows 7 kennt man das: die Form wird hell, dem Fenstertitel wird (keine Rückmeldung) angehängt, etc.
      Multithreading erlaubt es diese Schleife irgendwo anders auszuführen.

      Um es im Alltag zu zeigen: Man kocht gerade milch auf und muss gleichzeitig den Schnittlauch schneiden. Wenn man die Milch einfach stehen lässt und sich an den Schnittlauch macht wird die Milch inzwischen übergehen.
      Multithreading ist wie "Kannst Du den Schnittlauch für mich schneiden und mir die Schnipsel geben? Dann kann ich aufpassen, dass die Milch nicht übergeht."

      Auf ErfinderDesRades' Wunsch hin werden hier noch weitere Dinge hinzugefügt.
      Seine Ideen sind zwischen
      ____
      diesen
      ____
      Linien.

      Bevor wir uns ans Werk machen: Option Strict On!
      Nicht dass dann irgendwo steht "Dim Asdf As Integer = e.UserState(2)"

      Wenn das getan ist können wir anfangen:
      Erstellen wir zuerst eine neue Windows Forms Anwendung.
      Der Form fügen wir jetzt einen Button, ein NumericUpDown, eine ProgressBar und ein Label hinzu.
      Weil wir sauber programmieren geben wir den Controls auch anständige Namen. In meinem Fall Button_Start, NumericUpDown_Count, ProgressBar_Percentage und Label_Result (natürlich könnt Ihr eure Controls auch anders benennen, z.B. lblResult oder so).
      Dem Button geben wir noch den Text "Starten", dem Label den Text "Bereit" und dem NumericUpDown geben wir ein Minimum von 1 und ein Maximum von 1000000. Der ProgressBar wird der Maximumwert zur Laufzeit zugewiesen.
      Das war das Grundgerüst und jetzt kommen wir zum BackgroundWorker.

      Es gibt mehrere Möglichkeiten einen BackgroundWorker hinzuzufügen:
      Über die Toolbox unter "Komponenten"
      oder im Code.
      Ich bevorzuge die zweite Variante.

      Wechseln wir also zum Codefenster.

      Gleich als Erstes deklarieren wir den BackgroundWorker. Weil wir die Events benötigen mit dem Schlüsselwort WithEvents.

      VB.NET-Quellcode

      1. Dim WithEvents BGW As New System.ComponentModel.BackgroundWorker


      Weil der BackgroundWorker standardmäßig Eigenschaften hat, die wir nicht wollen hängen wir das With Schlüsselwort an und legen die gewünschten Eigenschaften fest.

      VB.NET-Quellcode

      1. Dim WithEvents BGW As New System.ComponentModel.BackgroundWorker With {.WorkerReportsProgress = True, .WorkerSupportsCancellation = True}


      Jetzt können wir ganz einfach alle Events des BackgroundWorkers über die beiden ComboBoxen am oberen Rand des Codefensters auswählen.
      In der Linken ComboBox wählen wir BGW und in der Rechten nacheinander DoWork, ProgressChanged und RunWorkerCompleted.

      Dadurch werden die Subs mit den passenden Parametern ganz bequem hinzugefügt.

      VB.NET-Quellcode

      1. Private Sub BGW_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BGW.DoWork
      2. End Sub
      3. Private Sub BGW_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BGW.ProgressChanged
      4. End Sub
      5. Private Sub BGW_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BGW.RunWorkerCompleted
      6. End Sub


      Was wir noch tun müssen ist eine einzige Sub hinzuzufügen.

      VB.NET-Quellcode

      1. Private Sub Start() Handles Button_Start.Click
      2. End Sub


      Als nächstes überlegen wir uns eine Aufgabe für den BackgroundWorker.
      Eine Schleife soll (Nur zur Darstellung) so oft durchlaufen, wie im NumericUpDown_Count angegeben wurde und bei jedem Durchlauf 10 Millisekunden warten.
      Gleichzeitig soll die ProgressBar_Percentage gefüllt werden und wenn die Aufgabe beendet wurde soll im Label_Result "Beendet" stehen.

      Nun müssen wir folgendes überlegen:
      • Dem BGW muss erklärt werden, wie oft die Schleife durchlaufen werden soll.
      • Der BGW muss der Form sagen, wie weit er mit dem Durchlaufen ist.
      • Der BGW muss der Form sagen, dass er fertig ist, sobald er fertig ist.


      Fangen wir mit dem ersten Punkt an: Dem BGW wird gesagt, wie oft die Schleife durchlaufen werden soll. Das können wir nur schlecht über eine Variable in der Form machen. Noch schlechter wäre direkt auf das Control zuzugreifen.
      Die richtige Methode ist dem BGW beim Starten ein Argument mitzugeben (Gleichzeitig wird ins Label_Result geschrieben, dass der Vorgang ausgeführt wird, der Maximumwert der ProgressBar wird gesetzt und sie wird auf 0 gesetzt).
      Schreiben wir also in die Sub Start:

      VB.NET-Quellcode

      1. Label_Result.Text = "Bitte warten..."
      2. ProgressBar_Percentage.Maximum = CInt(NumericUpDown_Count.Value)
      3. ProgressBar_Percentage.Value = 0
      4. BGW.RunWorkerAsync(CInt(NumericUpDown_Count.Value))


      Als Argument für .RunWorkerAsync kann ein Object übergeben werden, welches in der Sub BGW_DoWork als e.Argument zur Verfügung steht.
      Alle .Net Typen sind Object untergeordnet. Deshalb ist keine explizite Konvertierung von Integer in Object nötig (nicht so in die andere Richtung!).
      Der Grund für die Konvertierung von NumericUpDown_Count.Value (Typ Decimal) in Integer ist, dass die Schleife keine 3874.73805 mal durchlaufen werden kann (das letzte Viertel wird nicht ausgeführt^^), sondern nur Ganzzahlig. Natürlich wäre das mit For i As Decimal... lösbar, aber ProgressBar1.Value erlaubt auch nur Integer Werte und darum ist es sinnvoller gleich in Integer zu konvertieren (Wenn ich mich recht erinnere benötigt Decimal auch mehr Platz im Speicher. Zwar trivial bei einer Zahl aber trotzdem).

      Was passiert aber, wenn der BGW schon läuft? Es wird eine Exception geworfen "InvalidOperationException - Dieser BackgroundWorker ist derzeit ausgelastet und kann nicht mehrere Aufgaben gleichzeitig ausführen."
      Darum müssen wir vorher überprüfen, ob der BGW schon läuft. Und dazu müssen wir nicht selbst einen Boolean deklarieren und was weiß ich, sondern das .IsBusy Flag gibt an, ob der BGW ausgelastet ist. Darum:

      VB.NET-Quellcode

      1. If Not BGW.IsBusy Then
      2. Label_Result.Text = "Bitte warten..."
      3. ProgressBar_Percentage.Maximum = CInt(NumericUpDown_Count.Value)
      4. ProgressBar_Percentage.Value = 0
      5. BGW.RunWorkerAsync(CInt(NumericUpDown_Count.Value))
      6. End If


      ____
      Die Enabled Eigenschaft des Button_Start wird auf False gesetzt, wenn der BGW gestartet wird.
      Dadurch weiß der Benutzer, dass der Vorgang jetzt läuft und er das nicht zweimal gleichzeitig machen kann.
      ____

      VB.NET-Quellcode

      1. If Not BGW.IsBusy Then
      2. Label_Result.Text = "Bitte warten..."
      3. Button_Start.Enabled = False
      4. ProgressBar_Percentage.Maximum = CInt(NumericUpDown_Count.Value)
      5. ProgressBar_Percentage.Value = 0
      6. BGW.RunWorkerAsync(CInt(NumericUpDown_Count.Value))
      7. End If


      Damit wäre diese Sub fertig.

      ____
      Im ersten Fall bekommt der User gar kein Feedback, dass seine Aktion abgelehnt ist, und im zweiten ärgertersich über die Messagebox.

      Das hatte ich zuerst auch nicht gedacht und darum sollte man die MessageBox doch weglassen.
      ____

      Als nächstes kommt das, was so lange dauert: Die Schleife.

      VB.NET-Quellcode

      1. For i As Integer = 0 To
      2. System.Threading.Thread.Sleep(10)
      3. Next

      Ich habe hier absichtlich die Zahl nach To weggelassen und eine Zeile frei gelassen. Dazu später.

      Die For Schleife muss natürlich wissen, wann sie beendet werden soll. Wir erinnern uns an das übergebene Argument, e.Argument.
      e.Argument ist vom Typ Object. Also wer versucht To e.Argument zu schreiben wird von der IDE natürlich sofort darauf hingewiesen, dass das nicht geht (Bei Option Strict Off wäre das schon wieder durchgegangen).
      Weil wir wissen, dass als Argument nur Werte vom Typ Integer übergeben werden ist die Konvertierung einfach: CInt(e.Argument).
      Also sieht die Schleife schon so aus:

      VB.NET-Quellcode

      1. For i As Integer = 0 To CInt(e.Argument)
      2. System.Threading.Thread.Sleep(10)
      3. Next

      Kompliziertere Beispiele werden später noch erwähnt.

      Weil die Schleife jetzt wenn 100 übergeben wurde nicht 100 mal, sondern 101 mal durchläuft fügen wir nach CInt(...) noch - 1 an:

      VB.NET-Quellcode

      1. For i As Integer = 0 To CInt(e.Argument) - 1
      2. System.Threading.Thread.Sleep(10)
      3. Next


      Das sieht doch schon mal nicht schlecht aus. Aber die Form bekommt vom BGW nichts mit. Darum beschäftigen wir uns als nächstes mit der BGW_ProgressChanged Sub.

      In dieser Sub wird genau das gemacht, was wir jetzt machen: wir erzählen der Form, dass was passiert ist.
      Dazu rufen wir in der BGW_DoWork Sub nicht BGW_ProgressChanged auf, denn das würde bedeuten, dass sie im Thread des BGW aufgerufen wird, sondern wir verwenden BGW.ReportProgress.
      .ReportProgress erwartet als Argument einen Wert vom Typ Integer (genau das, was wir gesucht haben). Dieser wert ist standardmäßig im Bereich 0 bis 100, er kann aber auch darüber hinaus gehen.
      Wir werden logischerweise i als Argument übergeben.
      Darum erweitern wir unsere Schleife:

      VB.NET-Quellcode

      1. For i As Integer = 0 To CInt(e.Argument) - 1
      2. System.Threading.Thread.Sleep(10)
      3. BGW.ReportProgress(i)
      4. Next


      Was jetzt passiert ist: Im Thread der Form wird der Event BGW.ProgressChanged ausgelöst und die Sub BGW_ProgressChanged wird ausgeführt. Dort finden wir den angegebenen Wert (i) unter e.ProgressPercentage.
      Wir wollen diesen Wert in der ProgressBar anzeigen lassen, darum weisen wir ihn der ProgressBar zu:

      VB.NET-Quellcode

      1. ProgressBar_Percentage.Value = e.ProgressPercentage + 1

      Weil die For Schleife nur von 0 bis CInt(e.Argument) - 1 geht addieren wir 1 zum übergebenen Wert. Das heißt am Anfang wird der ProgressBar der Wert 0 und das Maximum m zugewiesen. Im Laufe der For Schleife nimmt der Wert zu. Beim Ersten Durchlauf 0 + 1, also 1, dann 1 + 1, also 2, dann 3, 4, und so weiter bis die For Schleife bei m - 1 angelangt ist. m - 1 wird übergeben, 1 wird addiert und wir haben m.
      Auch dazu habe ich ein paar kompliziertere Beispiele am Ende.

      Das ist alles, was in dieser Sub passiert.


      Das letzte was jetzt noch fehlt ist der Form zu sagen, dass der BGW fertig ist. Dazu müssen wir nicht mal was an der DoWork Sub ändern. Der Event BGW.RunWorkerCompleted wird automatisch ausgelöst, sobald die Sub BGW_DoWork beendet wurde.
      In der Sub BGW_RunWorkerCompleted ist alles was zu tun ist in das Label_Result zu schreiben, dass der BGW fertig ist.

      VB.NET-Quellcode

      1. Label_Result.Text = "Vorgang abgeschlossen"


      ____
      Weil der Button_Start beim Starten des BGW deaktiviert wurde muss das jetzt rückgängig gemacht werden
      ____

      VB.NET-Quellcode

      1. Label_Result.Text = "Vorgang abgeschlossen"
      2. Button_Start.Enabled = True


      Das war der Grundaufbau.

      Weil eine Nachricht nur 15000 Zeichen lang sein darf folgt der nächste Teil im zweiten Post.
      "Luckily luh... luckily it wasn't poi-"
      -- Brady in Wonderland, 23. Februar 2015, 1:56
      Desktop Pinner | ApplicationSettings | OnUtils

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Niko Ortner“ ()

      Teil 2:

      Jetzt folgen einige Erweiterungen, die nützlich sein können.
      • Abbrechen des Hintergrundvorgangs
      • Übergeben von als String darstellbare Werte an den BGW
      • Übergeben komplexer Werte an den BGW
      • Übergeben von als String darstellbare Werte vom BGW an die ProgressChanged Sub
      • Übergeben komplexer Werte and die ProgressChanged Sub


      Abbrechen des Hintergrundvorganges

      Da es manchmal erforderlich ist den Hintergrundvorgang abzubrechen zeige ich das auch noch.
      Das ist auch keine Hexerei.

      Als erstes fügen wir der Form einen weiteren Button hinzu: Button_Cancel mit dem Text "Abbrechen".
      Und die Sub Cancel mit dem Befehl BGW.CancelAsync:

      VB.NET-Quellcode

      1. Private Sub Cancel() Handles Button_Cancel.Click
      2. BGW.CancelAsync()
      3. End Sub


      ____
      Weil ein Vorgang, der nicht läuft auch nicht abgebrochen werden kann wird die Enabled Eigenschaft des Button_Cancel auf False gestellt.
      In der Sub Start wird die Enabled Eigenschaft dann auf True gesetzt.
      ____

      VB.NET-Quellcode

      1. If Not BGW.IsBusy Then
      2. Label_Result.Text = "Bitte warten..."
      3. Button_Start.Enabled = False
      4. Button_Cancel.Enabled = True
      5. ProgressBar_Percentage.Maximum = CInt(NumericUpDown_Count.Value)
      6. ProgressBar_Percentage.Value = 0
      7. BGW.RunWorkerAsync(CInt(NumericUpDown_Count.Value))
      8. End If


      Natürlich kann der BGW nicht einfach irgendwo mitten im Code aufhören. Das Ganze wird kontrolliert abgebrochen:
      In der Sub BGW_DoWork ist ein Boolean Wert unter BGW.CancellationPending verfügbar. Wenn BGW.CancelAsync aufgerufen wird wird dieser Wert auf True gesetzt.
      Falls das der Fall ist soll die Schleife beendet werden:

      VB.NET-Quellcode

      1. For i As Integer = 0 To CInt(e.Argument) - 1
      2. System.Threading.Thread.Sleep(10)
      3. BGW.ReportProgress(i)
      4. If BGW.CancellationPending Then
      5. Exit For
      6. End If
      7. Next


      Dass der Vorgang abgebrochen wurde ist auch in der BGW_RunWorkerCompleted Sub problemlos abfragbar.
      Danke an Seelenreiter für den Hinweis. e.Cancelled wird nicht auf True gesetzt. Warum genau das so ist weiß ich (noch) nicht. Laut MSDN könnte eine Race-Condition eintreten, wodurch einige Variablen nicht rechtzeitig gesetzt werden. Aber genaueres kann ich nicht sagen.
      Vielen Dank an dieser Stelle an markus.obi (Post #11). Er hat herausgefunden, dass in der BGW_DoWork Sub e.Cancel auf True gesetzt werden muss, damit es reibungslos läuft.

      Der Wert e.Cancelled in der BGW_RunWorkerCompleted Sub gibt an, ob der Vorgang abgebrochen wurde und anhand dessen kann man entscheiden, ob "Vorgang abgeschlossen" oder "Vorgang abgebrochen" ausgegeben werden soll.

      VB.NET-Quellcode

      1. If e.Cancelled Then
      2. Label_Result.Text = "Vorgang abgebrochen"
      3. Else
      4. Label_Result.Text = "Vorgang abgeschlossen"
      5. End If


      ____
      Weil der Vorgang jetzt erneut gestartet werden kann wird die Enabled Eigenschaft des Button_Start wieder auf True gesetzt.
      Und weil der Vorgang, der abgebrochen werden kann jetzt beendet ist wird die Enabled Eigenschaft des Button_Cancel jetzt wieder auf False gesetzt.
      ____

      VB.NET-Quellcode

      1. If e.Cancelled Then
      2. Label_Result.Text = "Vorgang abgebrochen"
      3. Else
      4. Label_Result.Text = "Vorgang abgeschlossen" 'Auch hier danke an Seelenreiter
      5. End If
      6. Button_Start.Enabled = True
      7. Button_Cancel.Enabled = False


      Danke an markus.obi für die Idee direkt in der BGW_DoWork Sub auf das Abbrechen zu reagieren.
      Im Bezug auf Post #11: Wenn abgebrochen wird, muss e.Cancel auf True gesetzt werden.

      VB.NET-Quellcode

      1. For i As Integer = 0 To CInt(e.Argument) - 1
      2. System.Threading.Thread.Sleep(10)
      3. BGW.ReportProgress(i)
      4. If BGW.CancellationPending Then
      5. e.Cancel = True 'Hier
      6. Exit For
      7. End If
      8. Next



      Übergeben von als String darstellbare Werte an den BGW

      Wenn man mehrere Parameter an den BGW übergeben will ist das recht einfach.
      Angenommen drei Integer Werte:

      VB.NET-Quellcode

      1. BGW.RunWorkerAsync({Parameter1, Parameter2, Parameter3})


      Diese kann man in der DoWork Sub wieder zerpflücken. Ich bevorzuge dabei diese Variante:

      VB.NET-Quellcode

      1. Dim Argument() As Integer = DirectCast(e.Argument, Integer())
      2. Dim Parameter1 As Integer = Argument(0)
      3. Dim Parameter2 As Integer = Argument(1)
      4. Dim Parameter3 As Integer = Argument(2)
      5. 'bzw wenn die Parameter jeweils nur einmal verwendet werden:
      6. Dim Argument() As Integer = DirectCast(e.Argument, Integer())
      7. 'Und als Beispiel:
      8. For i As Integer = 0 To Argument(0)
      9. For j As Integer = 0 To Argument(1)
      10. DoSomething(Argument(2))
      11. Next
      12. Next


      Bei Strings kann man gleich vorgehen, nur DirectCast(e.Argument, String()) verwenden

      Es kann auch sein, dass man Integer und String Werte gleichzeitig übergeben muss. Auch Booleanwerte lassen sich so übergeben.
      Einfach beim Aufrufen von .RunWorkerAsync alle Parameter, die keine Strings sind zu strings konvertieren:

      VB.NET-Quellcode

      1. BGW.RunWorkerAsync({StringWert, IntegerWert.ToString, BooleanWert.ToString})


      Und wieder auseinandernehmen:

      VB.NET-Quellcode

      1. Dim Argument() As String = DirectCast(e.Argument, String())
      2. Dim Parameter1 As String = Argument(0)
      3. Dim Parameter2 As Integer = CInt(Argument(1))
      4. Dim Parameter3 As Boolean = CBool(Argument(2))


      Das ist allerdings nur sinnvoll, wenn es nich zu viele Werte sind (mit 100 Werten wird es sehr unübersichtlich).
      Und es funktioniert nur mit bestimmten Werten. Ein Array kann z.B. nicht verwendet werden. Das Array {"Test", "Text", "Sonstwas"} ergibt als String mit der .ToString Methode "System.String()".
      Das funktioniert also nur bedingt.


      Übergeben komplexer Werte an den BGW

      Wenn komplexere Parameter übergeben werden sollen (z.B. Arrays, Rectangles, Regions, ganze Klassen) empfiehlt es sich auf eine eigene Klasse zurückzugreifen.

      Anstelle alles in ein Array zu werfen wird eine Instanz einer eigenen Klasse übergeben, die in der DoWork Sub nur einmal konvertiert werden muss.

      Dazu fügen wir irgendwo in einer Klasse eine eigene Klasse ein und deklarieren die benötigten Parameter (als Public).

      VB.NET-Quellcode

      1. Public Class BGWArgument
      2. Public StringParameter As String
      3. Public IntegerParameter As String
      4. Public BooleanParameter As Boolean
      5. Public RectangleParameter As Rectangle
      6. Public FormParameter As Form
      7. Public IntegerArrayParameter() As Integer
      8. Public Sonstwas As Object
      9. End Class


      Beim Aufrufen von .RunWorkerAsync übergeben wir eine Instanz dieser Klasse mit den Parametern:

      VB.NET-Quellcode

      1. BGW.RunWorkerAsync(New BGWArgument With {.StringParameter = "Text", .IntegerParameter = 3, .BooleanParameter = True, .RectangleParameter = New Rectangle(0,0,0,0), .FormParameter = New Form1, .IntegerArrayParameter = {1, 2, 3, 4, 14124, 5457342}, .Sonstwas = New System.ComponentModel.BackgroundWorker}) 'Ha, einen BackgroundWorker als Parameter übergeben^^
      2. 'Oder
      3. Dim Argument As New BGWArgument
      4. Argument.StringParameter = "Text"
      5. Argument.IntegerParameter = 3
      6. 'und so weiter
      7. BGW.RunWorkerAsync(Argument)


      Und in der BGW_DoWork Sub muss das Ganze nur einmal konvertiert werden:

      VB.NET-Quellcode

      1. Dim Argument As BGWArgument = DirectCast(e.Argument, BGWArgument)
      2. 'Und verwendet werden kann das so:
      3. For i As Integer = 0 To Argument.IntegerArgument
      4. IO.File.Create(Argument.StringParameter & i.ToString)
      5. Next
      6. 'Fantasie spielen lassen...



      Übergeben von als String darstellbare Werte vom BGW an die ProgressChanged Sub

      Das Übergeben der Werte an die ProgressChanged Sub ist dem Übergeben der Argumente an den BGW sehr ähnlich.
      Wieder werden alle Werte in Strings konvertiert und nachher wieder auseinandergepflückt.
      Es gibt nur den Unterschied, dass nicht der Parameter ProgressPercentage sondern UserState verwendet wird. Denn ProgressPercentage ist vom Typ Integer. UserState hingegen ist vom Typ Object.

      VB.NET-Quellcode

      1. For i As Integer = 0 To CInt(e.Argument) - 1
      2. System.Threading.Thread.Sleep(10)
      3. BGW.ReportProgress(Nothing, {"Text", i.ToString, True.ToString})
      4. Next

      Ich bevorzuge es hierbei für den Parameter ProgressPercentage Nothing anstelle von 0 zu übergeben, weil es offensichtlicher macht, dass der Parameter nicht verwendet wird.

      Das Auseinandernehmen funktioniert wie beim Übergeben der Argumente an den BGW:

      VB.NET-Quellcode

      1. Dim UserState() As String = DirectCast(e.UserState, String())
      2. Dim StringParameter As String = UserState(0)
      3. Dim IntegerParameter As Integer = CInt(UserState(1))
      4. Dim BooleanParameter As Boolean = CBool(UserState(2))
      5. 'Oder so:
      6. Dim UserState() As String = DirectCast(e.UserState, String())
      7. If CBool(UserState(2))
      8. Label_Result.Text = UserState(0) & ": " & UserState(1) 'Die Konvertierung in Integer entfällt hierbei, weil sowiso ein String benötigt wird.
      9. End If


      Auch hier muss man natürlich auf die möglichen Datentypen achten. Der Parameter {"Text", {"Lol", "Sonstwas"}.ToString, i.ToString} wird nicht wie gewünscht übergeben, weil {"Lol", "Sonstwas"}.ToString zu "System.String()" wird. Und damit kann man später auch nicht sehr viel anfangen.


      Übergeben komplexer Werte and die ProgressChanged Sub

      Auch das funktioniert wieder wie das Übergeben der Argumente an den BGW.

      Eine Klasse hinzufügen, in der alle benötigten Parameter deklariert sind, diese übergeben und anschliesend muss nur einmal konvertiert werden:

      VB.NET-Quellcode

      1. Class BGWUserState
      2. Public StringParameter As String
      3. Public IntegerParameter As String
      4. Public BooleanParameter As String
      5. Public FormParameter As String
      6. Public IntegerArrayParameter() As Integer
      7. Public ObjectParameter As Object
      8. End Class
      9. '...
      10. 'Für ObjectArgument kann natürlich alles übergeben werden. In diesem Fall einfach etwas zusammengewürfeltes.
      11. BGW.ReportProgress(Nothing, New BGWUserState With {.StringParameter = "Text", .IntegerParameter = i, .BooleanParameter = False, .FormParameter = New Form1, .IntegerArrayParameter = {1, 2, 41, 341313513, 391723498}, .ObjectParameter = New System.Reflection.AssemblyAlgorithmIdAttribute(System.Configuration.Assemblies.AssemblyHashAlgorithm.MD5)})


      Und die Verwendung:

      VB.NET-Quellcode

      1. Dim UserState As BGWUserState = DirectCast(e.UserState, BGWUserState)
      2. If UserState.BooleanParameter Then
      3. Label_Result = UserState.StringParameter
      4. Else
      5. 'Irgendwas
      6. End If






      Damit wäre dieses Tutorial fertig.

      Wenn jemand Verbesserungsvorschläge hat nehme ich diese gerne an.

      Und ja: es gibt auch die Methode "Dim T1 As New Thread(AdressOf ...)", diese habe ich aber absichtlich nicht angesprochen, weil Backgroundworker meines Erachtens einfacher zu verwenden sind, weil sie die nötigen Methoden von sich aus bereitstellen.
      Und Ja: es gibt die Möglichkeit CheckForIllegalCrossThreadCalls auf False zu stellen und somit die BGW_ProgressChanged Sub unnötig zu machen. Diese Methode ist aber sehr unsauber. Wie jemand schon mal geschrieben hat: Nicht auszumachen was passiert wenn der eine Thread gerade die Items der ListBox ausliest und der andere diese gerade verändert oder löscht.
      Dateien
      • BGW Test.rar

        (67,8 kB, 1.482 mal heruntergeladen, zuletzt: )
      "Luckily luh... luckily it wasn't poi-"
      -- Brady in Wonderland, 23. Februar 2015, 1:56
      Desktop Pinner | ApplicationSettings | OnUtils

      Dieser Beitrag wurde bereits 8 mal editiert, zuletzt von „Niko Ortner“ ()

      Niko Ortner schrieb:

      Was passiert aber, wenn der BGW schon läuft? Es wird eine Exception geworfen "InvalidOperationException - Dieser BackgroundWorker ist derzeit ausgelastet und kann nicht mehrere Aufgaben gleichzeitig ausführen."
      Darum müssen wir vorher überprüfen, ob der BGW schon läuft....Alternativ kann man auch eine MessageBox anzeigen lassen

      Beides keine schöne Useability. Im ersten Fall bekommt der User gar kein Feedback, dass seine Aktion abgelehnt ist, und im zweiten ärgertersich über die Messagebox.

      Besser wärs, den Button gleich zu disablen, der den Prozess startet. Dann sieht der User "Es passiert was", und gleichzeitig kannernix mehr anstellen, und versteht auch gleich, warum.
      Zusätzlich sollteman den Async-Cancel-Button enablen (falls Cancelation vorgesehen ist).

      Imo gehören 5 Elemente zum threading-Pattern:
      1. Disable Gui
      2. start RunAsync
      3. Update Gui
      4. ggfs. Cancelation
      5. (Async-finsished: ) Re-Enable Gui
      Auch dein UpdateGui-Interval von 10ms issn bischen flott - so schnell kann keiner gucken, das belastet nur die CPU.

      Ich mag den BGW ja ühaupt nicht, weil man eigentlich den Threading-Pattern in allen Punkten viel eleganter umsetzen könnte.
      Insbesondere das umständliche Transferieren von Werten in den jeweils anneren Thread ist mir ein Dorn im Auge (meine Lsg: AsyncWorker - CodeProject).

      Aber als Tut zum BGW findichs sehr gelungen. :thumbup:
      Vielen Dank für das Feedback. Ich werde das "Dis- und Enablen" des GUI noch hinzufügen (natürlich mit Verweis auf Dich).
      Ja, die 10 Millisekunden sind schon etwas schnell.
      "Luckily luh... luckily it wasn't poi-"
      -- Brady in Wonderland, 23. Februar 2015, 1:56
      Desktop Pinner | ApplicationSettings | OnUtils

      Hinweis und Frage

      Hallo Niko,

      in deinem Tutorial hat sich ein kleiner Typo eingeschlichen:

      VB.NET-Quellcode

      1. If e.Cancelled Then
      2. Label_Result.Text = "Vorgang abgebrochen"
      3. Else
      4. Label_Result.Text = "Vorgang abgeschlossen")
      5. End If


      Da ist ein ) zu viel. ;)

      Und jetzt zu meiner Frage.



      Dass der Vorgang abgebrochen wurde ist auch in der BGW_RunWorkerCompleted Sub problemlos abfragbar.
      Der Wert e.Cancelled gibt an, ob der Vorgang abgebrochen wurde und anhand dessen kann man entscheiden, ob "Vorgang abgeschlossen" oder "Vorgang abgebrochen" ausgegeben werden soll.

      an der Stelle liefert mir VB2010 aber e.cancelled = false zurück. ?(
      Any Hints?

      Greetz

      Seelenreiter

      Abbrechen des Hintergrundvorganges mit Info an den Hautthread

      Beim Abbrechen des BGW gibts ja das Problem, dass .CancellationPending nur in der DoWork Prozedur true sein kann.

      In der Completed Prozedur ist .CancellationPending immer false (das soll auch so sein denke ich!).
      Jedoch ist e.Cancelled dort auch immer false. Das hat nix mit einer Race Condition zu tun. Eine Race-Condition hast du, wenn der BGW abgebrochen wurde nachdem das letzte mal auf .CancellationPending überprüft wurde. Die Ausführung läuft durch wie geplant und der Abbruch wird in der Completed Prozedur nicht erkannt. Selbst wenn du die Abfrage If BackgroundWorker1.CancellationPending Then ans Ende deiner DoWork Prozedur setzen würdest, kann es dazu kommen. Das ist natürlich extrem unwahrscheinlich.

      Das Problem mit einer globalen Variable Cancelled kann einen Perfektionisten nicht zufriedenstellen.

      Neulich bin ich auf eine viel bessere - ich denke, die von Microsoft vorgesehene - Lösung gestoßen. Einfach e.Cancel in der DoWork Prozedur (!) auf true setzen.
      In der DoWork steht dann:

      VB.NET-Quellcode

      1. If BackgroundWorker1.CancellationPending Then
      2. e.Cancel = True
      3. Exit Sub 'oder Exit For
      4. End If

      Dann ist e.Cancelled in der Completed Prozedur auch auf true:

      VB.NET-Quellcode

      1. If e.Cancelled Then
      2. 'Abgebrochen
      3. Else
      4. 'Nicht Abgebrochen
      5. End If

      Man braucht keine globale Variable und alles ist wieder gut.
      Mir ist nochwas aufgefallen:

      Eine Exception, die in der DoWork Prozedur des BGWs fliegt, kann man nur in Visual Studio (Build: Debug oder Release egal) beim Debuggen erkennen.
      Wenn man die Anwendung normal startet, dann wird die Exception einfach verschluckt. Liegt wohl daran, dass der BGW in nem anderen Thread läuft.

      Normalerweise wird die Exception ja geworfen, was zu dem Fenster führt: Unbehandelte Ausnahme in der Anwendung... (BUTTONS: Details, Weiter, Beenden)

      Beim BGW landet die Ausführung einfach in der Completed Sub und e.Error enthält die Exception.
      Falls man irgendwo eine Exception erwartet hätte und genau diese auch behandeln kann, dann hätte man sie in der DoWork Schleife bereits gefangen und entsprechend behandelt.
      Nach dem Motto "Fange nur die Exceptions, die du auch behandeln kannst" sollte man die Exception in e.Error also werfen.

      Ergo:

      VB.NET-Quellcode

      1. Private Sub BackgroundWorker1_RunWorkerCompleted(sender As System.Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
      2. If e.Error IsNot Nothing Then
      3. Throw e.Error
      4. End If
      5. End Sub


      Klar spielt das fürs Debuggen keine Rolle. Aber so erhält der Anwender zumindest die Meldung, dass was schief gelaufen ist.
      Ich hatte neulich den Fall, als ich in nem BGW versucht habe nen leeren String in ein DateTime Objekt zu parsen. Da hab ich mich schon sehr gewundert wieso der Fortschrittsbalken stehen geblieben, aber keine Exception geflogen ist.
      @markus.obi: Danke für's testen. Bei mir funktioniert's problemlos:

      An der Framework-Version ligt's auch nicht. Ich hab alle Versionen durchprobiert. Bei jeder wir die Exception korrekt angezeigt.
      "Luckily luh... luckily it wasn't poi-"
      -- Brady in Wonderland, 23. Februar 2015, 1:56
      Desktop Pinner | ApplicationSettings | OnUtils
      Ich habe VS 2010 Ultimate und habs unter Windows 7 und 8 (jeweils 64bit Professional) getestet.
      Die .Net Versionen 3.5 Client, 4.0 Client und 4.0 Versionen habe ich durchprobiert.
      Alle möglichen Kombinationen habe ich jetzt nicht durch probiert. Aber das sollte auch nicht notwendig sein.

      Bei meinen Test wurde nie das Fenster "Unbehandelte Ausnahme" gezeigt.
      Ich vermute, dass die vom Kompiler erstellten Dateien identisch sind (bei gleicher .Net Framework Version). Sonst wäre der Kompiler Schrott.
      Also muss irgendwas zur Laufzeit anders sein. Vielleicht eine Einstellung im Betriebssystem.
      Ich habe es in VirtualBox unter Win8 getestet. Da war eigentlich nur .Net Framework für die Runtime installiert.

      Wir könnten mal die Binaries austauschen aber ich vermute stark, dass es am Rechner liegt.
      Bei mir wird die Exception ebenfalls verschluckt, wenn man die Exe direkt und ohne Debug-Umgebung startet.

      Nach meinem Kenntnisstand gehört das zu den Tücken von Threading (wies mitte Tasks ist und mittm Async weißichnich).
      Man muß besonderen Code schreiben für Exceptions, die in NebenThreads fliegen, weil ohne Debug-Umgebung (also erst im Produktiv-Betrieb) fliegen die ins Nirvana.

      Anbei mein Test
      Dateien
      @markus.obi: @ErfinderDesRades:
      Jetzt weiß ich, warum die Exception bei mir ausgelöst wird.
      Ich greife in der Completed-Sub auf e.Result zu. Sieht man sich das im Decompiler den Framework-Code an, sieht man das:

      Jetzt stellt sich nur noch die Frage, warum im Fensterchen nicht die TargetInvocationException, sondern die OverflowException anzeigt wird.

      Ich hab noch was getestet:

      VB.NET-Quellcode

      1. Dim T As Threading.Thread
      2. Protected Sub Start() Handles Button1.Click
      3. T = New Threading.Thread(AddressOf DoStuff)
      4. T.IsBackground = True
      5. T.Start()
      6. End Sub
      7. Private Sub DoStuff()
      8. For i = 0 To 512
      9. Dim Bla = CByte(i)
      10. Next
      11. End Sub

      Das ist das Ergebnis, wenn man direkt die Exe startet:

      Das Delegieren in den GUI-Thread bringt keine Veränderung:

      VB.NET-Quellcode

      1. Dim T As Threading.Thread
      2. Protected Sub Start() Handles Button1.Click
      3. T = New Threading.Thread(AddressOf DoStuff)
      4. T.IsBackground = True
      5. T.Start()
      6. End Sub
      7. Private Sub DoStuff()
      8. Try
      9. For i = 0 To 512
      10. Dim Bla = CByte(i)
      11. Next
      12. Catch ex As Exception
      13. Me.Invoke(Sub() Throw ex)
      14. End Try
      15. End Sub


      Hingegen: Ersetzt man das Invoke durch BeginInvoke, wird die Exception richtig angezeigt.

      Also darf die Exception nicht durch den Thread "hochgeworfen" werden. (Invoke kehrt erst zurück, sobald der Aufruf im anderen Thread fertig ist. BeginInvoke kehrt sofort zurück, behält also nicht den Kontext bei.)

      Da hat Microsoft gepfuscht.
      "Luckily luh... luckily it wasn't poi-"
      -- Brady in Wonderland, 23. Februar 2015, 1:56
      Desktop Pinner | ApplicationSettings | OnUtils
      Ich fasse mal zusammen:
      1. Bei dir wurde die Exception geworfen, weil die Funktion, die e.Result zurückgeben sollte die Exception e.Error wirft, falls sie nicht Nothing ist.
      2. Diese Funktion wirft außerdem eine InvalidOperationException wenn e.Cancel True ist. Wo wir wieder beim Thema wären ;) .

      Die Get-Funktion von e.Result wirft also schön Exceptions um sich rum, die man selber eigentlich eh vorher behandeln oder werfen könnte.
      Denn nicht immer benutzt der User e.Result, die Exception sollte aber immer geworfen werden.
      Ich würde allgemein so vorgehen:
      • kein zusätzliches kein Try Catch um den Code in DoWork
      • e.Error am Anfang der Completed Sub selber werfen
      • e.Cancelled selber überprüfen und im Falle eines Abbruchs NICHT auf e.Result zugreifen. Falls man das (unfertige) e.Result auch im Falle eines Abbruchs haben will, so MUSS man darauf verzichten e.Cancel auf True zu setzen. Siehe Codebeispiel 2. Dann müsste man allerdings eine globale Variable Cancelled verwenden um den Abbruch zu erkennen... Oder man schreibt das Ergebnis gleich in eine globale Variable...

      Niko Ortner schrieb:

      Invoke kehrt erst zurück, sobald der Aufruf im anderen Thread fertig ist. BeginInvoke kehrt sofort zurück, behält also nicht den Kontext bei

      Ich vermute mal, dass die Exception bei Invoke verhindert, dass die Ausführung in den BGW-Thread zurückkehrt, weil ja eine Exception alle Prozeduren abbricht. Dabei geht wohl irgendwas schief.
      Es ist ja eh nicht notwendig die Exception direkt in der DoWork Prozedur zu fangen, NUR um sie zu werfen. Try macht das ganze nur langsamer - werfen kann man die Exception auch noch in der Completed Prozedur.

      Mein Vorschlag

      VB.NET-Quellcode

      1. Private WithEvents bgw1 As New BackgroundWorker With {.WorkerSupportsCancellation = True}
      2. Private Sub bgw1_DoWork(sender As System.Object, e As System.ComponentModel.DoWorkEventArgs) Handles bgw1.DoWork
      3. Dim intlist As New List(Of Integer)
      4. For i = 1 To 20
      5. If bgw1.CancellationPending Then
      6. e.Cancel = True
      7. Exit Sub
      8. End If
      9. intlist.Add(i)
      10. Threading.Thread.Sleep(100)
      11. ' "Künstliche" Exception:
      12. ' intlist.Item(-1) = 0
      13. Next
      14. e.Result = intlist
      15. End Sub
      16. Private Sub bgw1_Completed(sender As System.Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles bgw1.RunWorkerCompleted
      17. If e.Error IsNot Nothing Then
      18. Throw e.Error
      19. Else
      20. If e.Cancelled Then
      21. 'Code wenn BGW Abgebrochen wurde
      22. MessageBox.Show("Vorgang wurde abgebrochen")
      23. Else
      24. 'e.Result kann benutzt werden, ohne dass einem vorhersehbare Exceptions um die Ohren fliegen
      25. MessageBox.Show("Length of resulting List: " & DirectCast(e.Result, List(Of Integer)).Count.ToString)
      26. End If
      27. End If
      28. End Sub

      Steht auch auf MSDN:
      Der RunWorkerCompleted-Ereignishandler sollte vor dem Zugreifen auf die Result-Eigenschaft immer die Error-Eigenschaft und die Cancelled-Eigenschaft überprüfen. Wenn eine Ausnahme ausgelöst oder der Vorgang abgebrochen wurde, löst das Zugreifen auf die Result-Eigenschaft eine Ausnahme aus.


      Wenn man e.Result auch im Falle eines Abbruchs haben will

      VB.NET-Quellcode

      1. Private WithEvents bgw1 As New BackgroundWorker With {.WorkerSupportsCancellation = True}
      2. Private Sub bgw1_DoWork(sender As System.Object, e As System.ComponentModel.DoWorkEventArgs) Handles bgw1.DoWork
      3. Dim intlist As New List(Of Integer)
      4. e.Result = intlist
      5. For i = 1 To 20
      6. If bgw1.CancellationPending Then
      7. 'e.Cancel wird NICHT gesetzt
      8. Exit Sub
      9. End If
      10. intlist.Add(i)
      11. Threading.Thread.Sleep(100)
      12. ' "Künstliche" Exception:
      13. ' intlist.Item(-1) = 0
      14. Next
      15. End Sub
      16. Private Sub bgw1_Completed(sender As System.Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles bgw1.RunWorkerCompleted
      17. MessageBox.Show("Length of resulting List: " & DirectCast(e.Result, List(Of Integer)).Count.ToString)
      18. 'Exception wird automatisch geworfen
      19. 'einen Abbruch kann man hier nicht erkennen (außer mit globaler Variable)
      20. End Sub