Threading: Mini-Projekt / Verständnisfrage

  • VB.NET
  • .NET (FX) 4.5–4.8

Es gibt 11 Antworten in diesem Thema. Der letzte Beitrag () ist von tragl.

    Threading: Mini-Projekt / Verständnisfrage

    Hi, ich hab ein Mini-Projekt (eigentlich ein Updater für meine große Anwendung), welches von meiner Anwendung gestartet wird (wenn's ein Update gibt)
    und dann stumpf die Dateien vom Server zum Client kopiert. Dabei werden alle vorhandenen Files überschrieben (eigentlich sollen die sogar vorher noch gelöscht
    werden, das werd' ich noch einbauen)

    Die Methode für's Kopieren müsste nun so laufen, dass sich die Richtextbox korrekt updatet und auch die Progressbar durchläuft (Marquee)
    Meine Frage wäre nun, was müsste ich in einen Nebenthread packen, damit das auch so funzt? Ich würde Tippen, das Hinzufügen von Text zur RichTextBox und das Kopieren selbst?

    VB.NET-Quellcode

    1. Private Sub CopyAllFilesAndSubfoldersFromSourcepathToDestinationpath(Sourcepath As String, Destinationpath As String)
    2. 'TODO: Mit Threading arbeiten, damit rtb sich sauber updatet und die Progressbar durchläuft
    3. ProgressBar1.Visible = True
    4. If Not Directory.Exists(Destinationpath) Then Directory.CreateDirectory(Destinationpath)
    5. Dim RootFiles = Directory.GetFiles(Sourcepath)
    6. For Each FullFilename In RootFiles
    7. Try
    8. RichTextBox1.AddRtxt($"Datei ""{FullFilename.LastRightCut("\"c)}"" wird kopiert...")
    9. File.Copy(FullFilename, $"{Destinationpath}\{FullFilename.LastRightCut("\"c)}", True)
    10. Catch ex As Exception
    11. RichTextBox1.AddRtxt($"Fehler für Datei ""{FullFilename.LastRightCut("\"c)} "" {Environment.NewLine}{ex.Message}")
    12. _ErrorCounter += 1
    13. End Try
    14. Next
    15. Dim SubDirs = Directory.GetDirectories(Sourcepath)
    16. For Each Foldername In SubDirs
    17. RichTextBox1.AddRtxt($"{Environment.NewLine}Unterverzeichnis ""{Foldername.LastRightCut("\"c)}"" wird kopiert...")
    18. CopyAllFilesAndSubfoldersFromSourcepathToDestinationpath(Foldername, $"{Destinationpath}\{Foldername.LastRightCut("\"c)}")
    19. Next
    20. ProgressBar1.Visible = False
    21. End Sub


    VB.NET-Quellcode

    1. ''' <summary>Fügt einer RichTextBox einen Text hinzu</summary>
    2. <Extension>
    3. Public Sub AddRtxt(rtb As RichTextBox, Text As String)
    4. rtb.AppendText($"{Text}{Environment.NewLine}")
    5. rtb.ScrollToCaret()
    6. End Sub


    Auf der Form gibt's nur eine RichTextBox und ne ProgressBar, sonst nix weiter.

    "Na, wie ist das Wetter bei dir?"
    "Caps Lock."
    "Hä?"
    "Shift ohne Ende!" :thumbsup:
    Ich sach mal: Alles kommt in den Nebenthread, außer die RichTextBox1.AddRtxt-Zeilen. Da sollte der Nebenthread den GUI-Thread per Event über Änderungen informieren. In dem Atemzug soll der GUI-Thread auch gleich derdiedas Progressbar aktualisieren.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    @VaporiZed: Danke erstmal. Progressbar braucht nicht aktualisiert werden, die läuft dann im Marquee.
    Hast du einen Tipp für mich, wie ich das gescheit anstelle? Sicher, dass die AddRtxt-Zeilen nicht nebenläufig müssen? In dem aktuellen Code oben
    tauchen die hinzugefügten Zeilen dann irgendwann mal auf - soll aber eigentlich zeitnah passieren.

    Zur Not kann ich auch erst alles durchlaufen lassen und gebe später alles als "Protokoll" aus - wäre jetzt kein Beinbruch. Aber schöner wär's ja,
    wenn man sieht was da im Hintergrund gemacht wird.

    Mir fällt grad ein, die Progressbar könnte sogar komplett verschwinden. Statt dessen könnte ich ja auch nen WaitCursor setzen.
    "Na, wie ist das Wetter bei dir?"
    "Caps Lock."
    "Hä?"
    "Shift ohne Ende!" :thumbsup:

    tragl schrieb:

    Sicher, dass die AddRtxt-Zeilen nicht nebenläufig müssen?
    Wenn Du die nebenläufig machen würdest, gäbe es einen Crash wegen threadübergreifenden Vorgängen. Von daher: ja, ich bin mir sicher. Wenn Du das Informationsevent ans Zielform schickst, musst Du im Zielform mit Invoke (nicht BeginInvoke) arbeiten, damit das GUI aktualisiert wird und es danach erst mit der nächsten Datei weitergeht.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    @VaporiZed:

    Hab's jetzt mal so gebastelt, aber bringt nicht das gewünscht Ergebnis. Läuft zwar durch - es werden auch alle Dateien und Ordner kopiert, aber in der
    RichTextBox tut sich erst zum Ende was... Ich bin mit dem Threading-Kram echt überfordert. Das hätte MS auch einfacher lösen können :P

    VB.NET-Quellcode

    1. Private Sub CopyAllFilesAndSubfoldersFromSourcepathToDestinationpath(Sourcepath As String, Destinationpath As String)
    2. Directory.CreateDirectory(Destinationpath)
    3. Dim FolderRootFiles = Directory.GetFiles(Sourcepath)
    4. For Each PathAndFilename In FolderRootFiles
    5. Try
    6. Me.Invoke(Sub() RichTextBox1.AddRtxt($"Datei ""{PathAndFilename.LastRightCut("\"c)}"" wird kopiert..."))
    7. Dim thr As Thread
    8. thr = New Thread(Sub() FileCopy(PathAndFilename, Destinationpath))
    9. With thr
    10. .IsBackground = True
    11. .SetApartmentState(ApartmentState.STA)
    12. .Start()
    13. End With
    14. Thread.Sleep(100) 'Da die Operation ohne Server recht zügig geht, ein Sleep um zu sehen, ob sich was tut.
    15. Catch ex As Exception
    16. RichTextBox1.AddRtxt($"Fehler für Datei ""{PathAndFilename.LastRightCut("\"c)} "" {Environment.NewLine}{ex.Message}")
    17. _ErrorCounter += 1
    18. End Try
    19. Next
    20. Dim SubDirs = Directory.GetDirectories(Sourcepath)
    21. For Each Foldername In SubDirs
    22. RichTextBox1.AddRtxt($"{Environment.NewLine}Unterverzeichnis ""{Foldername.LastRightCut("\"c)}"" wird kopiert...")
    23. CopyAllFilesAndSubfoldersFromSourcepathToDestinationpath(Foldername, $"{Destinationpath}\{Foldername.LastRightCut("\"c)}")
    24. Next
    25. End Sub
    26. Private Sub FileCopy(PathAndFilename As String, Destinationpath As String)
    27. File.Copy(PathAndFilename, $"{Destinationpath}\{PathAndFilename.LastRightCut("\"c)}", True)
    28. End Sub
    "Na, wie ist das Wetter bei dir?"
    "Caps Lock."
    "Hä?"
    "Shift ohne Ende!" :thumbsup:
    Moment. Befindet sich die Sub in der Form-Klasse? Dann brauchst Du natürlich auch kein Event:

    VB.NET-Quellcode

    1. Private Async Sub BtnWendeUpdateAn_Click(sender As Object, e As EventArgs) Handles BtnWendeUpdateAn.Click
    2. 'ruft die Kopiersub so auf, dass sie nebenläufig durchgeführt wird:
    3. Await Threading.Tasks.Task.Run(Sub() CopyAllFilesAndSubfoldersFromSourcepathToDestinationpath(Sourcepath, Destinationpath))
    4. End Sub
    5. Private Sub CopyAllFilesAndSubfoldersFromSourcepathToDestinationpath(Sourcepath As String, Destinationpath As String)
    6. Directory.CreateDirectory(Destinationpath)
    7. Dim FolderRootFiles = Directory.GetFiles(Sourcepath)
    8. For Each PathAndFilename In FolderRootFiles
    9. Try
    10. AddToLoggingRTB(RichTextBox1.AddRtxt($"Datei ""{PathAndFilename.LastRightCut("\"c)}"" wird kopiert …"))
    11. FileCopy(PathAndFilename, Destinationpath))
    12. Catch ex As Exception
    13. AddToLoggingRTB(RichTextBox1.AddRtxt($"Fehler für Datei ""{PathAndFilename.LastRightCut("\"c)} "" {Environment.NewLine}{ex.Message}")
    14. _ErrorCounter += 1
    15. End Try
    16. Next
    17. Dim SubDirs = Directory.GetDirectories(Sourcepath)
    18. For Each Foldername In SubDirs
    19. AddToLoggingRTB($"{Environment.NewLine}Unterverzeichnis ""{Foldername.LastRightCut("\"c)}"" wird kopiert..."))
    20. CopyAllFilesAndSubfoldersFromSourcepathToDestinationpath(Foldername, $"{Destinationpath}\{Foldername.LastRightCut("\"c)}")
    21. Next
    22. End Sub
    23. Private Sub FileCopy(PathAndFilename As String, Destinationpath As String)
    24. File.Copy(PathAndFilename, $"{Destinationpath}\{PathAndFilename.LastRightCut("\"c)}", True)
    25. End Sub
    26. Private Sub AddToLoggingRTB(Text As String)
    27. Me.Invoke(Sub() RichTextBox1.AddRtxt(Text))
    28. End Sub


    ungetestet
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    @VaporiZed ja, ganz vergessen zu erwähnen. Es gibt nur eine frmMain und da ist alles drauf. Da ich einen Button nutze, optional aber via Startparameter den Updateprozess direkt starte hab ich es nun wie folgt gelöst:

    Spoiler anzeigen

    VB.NET-Quellcode

    1. ''' <summary>Startet den Update-Vorgang, sofern die lokale Version nicht in Benutzung ist</summary>
    2. Private Async Sub UpdateApplication()
    3. If Not ApplicationIsInUse() Then
    4. Await Task.Run(Sub() CopyAllFilesAndSubfoldersFromSourcepathToDestinationpath(_ServerPath, _LocalPath))
    5. If _ErrorCounter > 0 Then
    6. msgExclamation("Beim Update sind Fehler aufgetreten, bitte an den Administartor wenden!")
    7. Return
    8. End If
    9. If msgQuestionInfo("Update abgeschlossen, soll die Anwendung nun gestartet werden?") = DialogResult.Yes Then
    10. Dim LtoolProcess As New Process
    11. LtoolProcess.StartInfo.FileName = _LocalAppfileAndPath
    12. LtoolProcess.Start()
    13. Me.Close()
    14. Else
    15. Me.Close()
    16. End If
    17. Else
    18. msgExclamation("Die Anwendung ist derzeit in Benutzung. Bitte alle Instanzen (auch die von anderen Benutzern auf dem gleichen Rechner) schließen und erneut versuchen!")
    19. Me.Close()
    20. End If
    21. End Sub
    22. ''' <summary>Kopiert alle Dateien vom Serverpfad zum Clientpfad. Bereits vorhandene werden überschrieben</summary>
    23. Private Sub CopyAllFilesAndSubfoldersFromSourcepathToDestinationpath(Sourcepath As String, Destinationpath As String)
    24. 'TODO: alte Files vorher löschen, falls sich die anzahl der Dateien mal verringert, dann sind diese wenigstens direkt mit weg. Alternativ vergleichen und nur nicht mehr vorhandene löschen
    25. Directory.CreateDirectory(Destinationpath)
    26. Dim FolderRootFiles = Directory.GetFiles(Sourcepath)
    27. For Each PathAndFilename In FolderRootFiles
    28. Try
    29. AddLogToRichtextbox($"Datei ""{PathAndFilename.LastRightCut("\"c)}"" wird kopiert...")
    30. File.Copy(PathAndFilename, $"{Destinationpath}\{PathAndFilename.LastRightCut("\"c)}", True)
    31. Catch ex As Exception
    32. RichTextBox1.AddRtxt($"Fehler für Datei ""{PathAndFilename.LastRightCut("\"c)} "" {Environment.NewLine}{ex.Message}")
    33. _ErrorCounter += 1
    34. End Try
    35. Next
    36. Dim SubDirs = Directory.GetDirectories(Sourcepath)
    37. For Each Foldername In SubDirs
    38. AddLogToRichtextbox($"{Environment.NewLine}Unterverzeichnis ""{Foldername.LastRightCut("\"c)}"" wird kopiert...")
    39. CopyAllFilesAndSubfoldersFromSourcepathToDestinationpath(Foldername, $"{Destinationpath}\{Foldername.LastRightCut("\"c)}")
    40. Next
    41. End Sub
    42. Private Sub AddLogToRichtextbox(Text As String)
    43. Me.Invoke(Sub() RichTextBox1.AddRtxt(Text))
    44. End Sub
    45. ''' <summary>Überprüft, ob die lokale Anwendung gerade in Benutzung ist</summary>
    46. Private Function ApplicationIsInUse() As Boolean
    47. Try
    48. Using fs = File.OpenWrite(_LocalAppfileAndPath)
    49. End Using
    50. Return False
    51. Catch
    52. Return True
    53. End Try
    54. End Function


    Danke! :thumbup: Das kann ich bestimmt noch an anderen Stellen im Hauptprogramm nutzen ;)

    EDIT: @VaporiZed: Falls du das obige schon gelesen hast.
    Nur nochmal für mein Verständnis:

    Da sich alles auf einer Form bzw. in der FormKlasse befindet, schreibe ich eine Async Sub, worin dann alles mit dem Aufruf Await Task.Run(AsyncSub) in einem anderen Thread
    als dem Hauptthread ausgeführt wird? Sind darin dann Stellen enthalten, die die GUI verändern muss ich das über Me.Invoke(...) lösen, damit wäre der GUI-Thread betroffen?

    LG

    "Na, wie ist das Wetter bei dir?"
    "Caps Lock."
    "Hä?"
    "Shift ohne Ende!" :thumbsup:

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

    tragl schrieb:

    Da sich alles auf einer Form bzw. in der FormKlasse befindet, schreibe ich eine Async Sub, worin dann alles mit dem Aufruf Await Task.Run(AsyncSub) in einem anderen Thread
    als dem Hauptthread ausgeführt wird? Sind darin dann Stellen enthalten, die die GUI verändern muss ich das über Me.Invoke(...) lösen, damit wäre der GUI-Thread betroffen?
    Ja genau.
    Da hast du einen guten Abriss der Vorgehensweise formuliert.
    Anzumerken noch, dass .Invoke den Nebenthread ausbremst - also der wartet dann, bis der MainThread das Invokete ausgeführt hat.
    .BeginInvoke wartet nicht, ist also üblicherweise die performantere Variante.
    Ausserdem anzumerken, dass man mit Invoken vergleichsweise sparsam umgehen sollte, weil jeder Threadwechsel braucht viel Resourcen.
    Ggfs. lieber erstmal einen Haufen anzuzeigender Daten sammeln, damit sich dann der Threadwechsel auch lohnt.
    Häufiger als ca. alle 300ms sollten die Invokes nicht auftreten, denn so schnell kann eh keiner gucken.

    ErfinderDesRades schrieb:

    Ausserdem anzumerken, dass man mit Invoken vergleichsweise sparsam umgehen sollte

    jo, das kann ich mir gut vorstellen. Allerdings ist das Programm tatsächlich so winzig und die heutigen PC's so "gut", dass man das in dem Fall tatsächlich vernachlässigen kann. Bei anderen Update- oder Installationsprogrammen, die einen Kopiervorgang starten
    kann man auch nix lesen, weil's so schnell geht. Wird da ja vermutlich nicht anders gelöst sein. Aber ich merk' mir das für mein großes Hauptprogramm ;)

    ErfinderDesRades schrieb:

    Häufiger als ca. alle 300ms sollten die Invokes nicht auftreten, denn so schnell kann eh keiner gucken.

    was macht man denn, wenn man nicht weiß wie "schnell" das da abläuft? Also bei variablen Sachen?
    "Na, wie ist das Wetter bei dir?"
    "Caps Lock."
    "Hä?"
    "Shift ohne Ende!" :thumbsup:
    Moinsen!

    Das mit der Wiederholfrequez ist ganz simpel: Im feuernden Thread den Zeitpunkt des letzten Feuerns merken und das nächste Ereignis erst nach Ablauf des (einstellbaren) Intervalls feuern (sofern sich was geändert hat)

    Viele Grüße

    Gerrit

    ErfinderDesRades schrieb:

    habich das mit einer Erweiterung der Progress(Of T)-Klasse einfürallemal gelöst.


    Sieht gut aus, da les' ich mich mal ein wenn ich Zeit habe :thumbup:
    "Na, wie ist das Wetter bei dir?"
    "Caps Lock."
    "Hä?"
    "Shift ohne Ende!" :thumbsup: