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.
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.
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.
Was wir noch tun müssen ist eine einzige Sub hinzuzufügen.
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:
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:
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:
____
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.
____
Damit wäre diese Sub fertig.
____
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.
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:
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:
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:
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:
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.
____
Weil der Button_Start beim Starten des BGW deaktiviert wurde muss das jetzt rückgängig gemacht werden
____
Das war der Grundaufbau.
Weil eine Nachricht nur 15000 Zeichen lang sein darf folgt der nächste Teil im zweiten Post.
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.
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.
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
- Private Sub BGW_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BGW.DoWork
- End Sub
- Private Sub BGW_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BGW.ProgressChanged
- End Sub
- Private Sub BGW_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BGW.RunWorkerCompleted
- End Sub
Was wir noch tun müssen ist eine einzige Sub hinzuzufügen.
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:
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:
____
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.
____
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.
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:
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:
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:
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:
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.
____
Weil der Button_Start beim Starten des BGW deaktiviert wurde muss das jetzt rückgängig gemacht werden
____
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
-- Brady in Wonderland, 23. Februar 2015, 1:56
Desktop Pinner | ApplicationSettings | OnUtils
Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Niko Ortner“ ()