none
[VB/Windows] Multithreading mit UI Update RRS feed

  • Frage

  • Hallo  zusammen,

    stellt euch vor, ich habe 100 einzelne, zeitintensive Aufgaben. Diese nacheinander abzuarbeiten würde eine lange Zeit benötigen. Daher will ich diese 100 Aufgaben durch Multithreading so gut wie möglich nebeneinander ausführen lassen. Meine Versuche mittels einzelnen Threads und ThreadPool haben bereits funktioniert.

    Nur muss ich diesen Pool an Threads nun so implementieren, dass er im Hintergrund abgearbeitet wird und dabei die UI aktualisiert wird.

    Mein Plan, den Pool einfach über einen BackgroundWorker laufen zu lassen, hat leider nicht funktioniert. Die UI war trotzdem eingefroren.

    Gleiches gilt für den Plan, für jede Aufgabe einen BackgroundWorker laufen zu lassen.

    Ich brauche bitte Hilfe durch Ideen oder konkreten Antworten, wie ich diesen "Hintergrund-Multihread" implementieren kann.

    Danke, mfg.


    Wupp Wupp! Fnak Fnak!

    Mittwoch, 20. März 2019 01:05

Antworten

  • Hi Martin,
    nachfolgend mal eine kleine Demo zu Deinem Problem.

    Ich habe mal 2 Execute-Methoden dargestellt, eine mit der Thread-Klasse ohne Begrenzung des Thread-Pools, eine mit BeginInvoke des Delegaten. Wichtig dabei ist, dass das Datensatz-Objekt threadsicher ist. Solange darin nur Eigenschaften genutzt werden, die Wertetypen repräsentieren, ist das auch gewährleistet.

    Bei komplexen Typen in der Klasse Datensatz muss die Thread-Sicherheit geprüft werden. Im Extremfall muss bei fehlender Thread-Sicherheit in der Ereignisroutine eine tiefe Kopie genutzt werden, d.h. es muss im Ausgangs-Thread (UI-Thread) das Datensatz-Objekt instanziiert und mit Werten gefüllt werden. Das könnte man im Aufruf der Ereignisroutine machen. Das bedeutet aber eine Verzögerung, die zu betrachten und ggf. zu minimieren ist.

    Imports System.Runtime.Remoting.Messaging
    Imports System.Threading
    
    Public Class Form07
    
      ' ohne Designer in leerer Form
      Private pan As New FlowLayoutPanel With {.Dock = DockStyle.Fill}
    
      Private Sub Form07_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.Controls.Add(pan)
        ' Verarbeitung nach dem Laden des Forms starten
        StartWork()
      End Sub
    
      Private rnd As New Random ' Zufallszahl für Zeitdauer der Verarbeitung
    
      Private Sub StartWork()
        For i = 1 To 100
          Dim c As New AsyncExecuter ' neue Verarbeitungsklasse erzeugen
          ' Delegate zuweisen
          c.Aufgabe = New Func(Of Integer, Datensatz)(Function(par As Integer) As Datensatz
                                                        ' eine zufällig Verarbeitungsdauer simulieren
                                                        Thread.Sleep(rnd.Next(100, 5000))
                                                        ' das Ergebnis zurückliefern 
                                                        Return New Datensatz With {.ID = par, .Info = $"Data {Thread.CurrentThread.ManagedThreadId}"}
                                                      End Function)
          ' Ende-Ereignis der Verarbeitung behandeln
          AddHandler c.AufgabeFertig, AddressOf C_Fertig
          ' Start der Ausführung
          c.Execute(i)
          ' c.Execute2(i) ' alternative Möglichkeit mit BeginInvoke
        Next
      End Sub
    
      ' Die Ereignismethode wird gleichen Context ausgeführt
      ' damit werden thread-übergreifende Zugriffe unterbunden
      ' Das sichert die Nutzung des SynchronizationContext
      Private Sub C_Fertig(sender As Object, e As EventArgs)
        Dim c = TryCast(sender, AsyncExecuter) ' den Typ des Ereignisauslösers ermitteln
        If c Is Nothing Then Exit Sub ' Sicherheit, falls falsch nicht erwarteter Typ
        ' Steuerelement instanziieren und mit Inhalt füllen
        Dim ctl As New Label With {.Text = $"{c.Ergebnis.ID} - {c.Ergebnis.Info}"}
        ' Steuerelement der Oberfläche hinzufügen
        pan.Controls.Add(ctl)
      End Sub
    
    
      Public Class AsyncExecuter
    
        ' zum Zeitpunkt der Instanziierung wird der aktuelle Context (des Aufrufers) gemerkt,
        ' um spätere Callbacks (Ereignisse) in den Context des Aufrufers zur Abarbeitung einzuordnen
        Private sc As SynchronizationContext = SynchronizationContext.Current
    
        Public Property Aufgabe As Func(Of Integer, Datensatz)
        Public Property Ergebnis As Datensatz
    
        Public Event AufgabeFertig As EventHandler
    
        ' Nutzung der Thread-Klasse
        Public Sub Execute(parameter As Integer)
          ' Hier wird die Aufgaben-Eigenschaft asynchron aufgerufen und
          ' die Rückgabe der Ergebnis-Eigenschaft zugewiesen.
          ' Anschließend wird das AufgabeFertig-Ereignis ausgelöst.
    
          ' Dazu ein neues Thread-Objekt erstellen und starten
          Call (New Thread(New ParameterizedThreadStart(Sub() ' Methode, die im Thread ausgeführt wird
                                                          ' die übergebene Func aufrufen und Ergebnis zuweisen
                                                          Ergebnis = Aufgabe.Invoke(parameter)
                                                          ' im Context des Ausgangsthreads ausführen, um thread-übergreifende Zugriffe auszuschließen
                                                          sc.Post(New SendOrPostCallback(Sub(state As Object)
                                                                                           ' Ereignismethode ausführen
                                                                                           RaiseEvent AufgabeFertig(Me, New EventArgs)
                                                                                         End Sub), parameter)
                                                        End Sub))).Start()
        End Sub
    
        ' Nutzung der asynchronen Verarbeitungsmöglichkeiten des Delagates
        Public Sub Execute2(parameter As Integer)
          Aufgabe.BeginInvoke(parameter, AddressOf Execute2CB, Aufgabe)
        End Sub
    
        Private Sub Execute2CB(ar As IAsyncResult)
          Dim aresult = TryCast(ar, AsyncResult) ' zum Typ AsyncResult casten
          Dim del = TryCast(aresult.AsyncDelegate, Func(Of Integer, Datensatz)) ' den Delegate der Func holen
          Ergebnis = del.EndInvoke(ar) ' das Ergebnis abholen
          ' in den Context des Ausgangsthreads einordnen, um thread-übergreifende Zugriffe auszuschließen
          sc.Post(New SendOrPostCallback(Sub(state As Object)
                                           ' Ereignismethode ausführen
                                           RaiseEvent AufgabeFertig(Me, New EventArgs)
                                         End Sub), Nothing)
        End Sub
      End Class
    
      ' Wichtig ist, dass Objekte dieses Typs (Klasse) threadsicher sind.
      ' Da die Backingfields der Eigenschaften Wertetypen sind, sind auch Objekte
      ' vom Typ dieser Klasse threadsicher.
      Public Class Datensatz
        Public Property ID As Integer
        Public Property Info As String
      End Class
    
    End Class


    --
    Viele Grüsse / Best Regards
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks

    Samstag, 23. März 2019 05:49

Alle Antworten

  • Hi Martin,
    geht es um ein Windows Forms Projekt oder um ein WPF Projekt? Ich frage, weil ich da ein passendes Beispiel beitragen könnte.

    Handelt es sich bei den asynchronen Aufgaben um Methoden ohne Rückgabe-Wert oder Methoden mit Rückgabewert? Wenn mit Rückgabewert, was wird zurückgegeben und ist das auch threadsicher im UI-Thread, falls daraus Anzeigen getätigt werden sollen?


    --
    Viele Grüsse / Best Regards
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks

    Mittwoch, 20. März 2019 12:16
  • Hi Peter,

    bei meiner Anwendung handelt es sich um ein Windows Forms Projekt.

    Zurzeit ist es so konstruiert, dass die einzelnen asynchronen Funktionen Daten analysieren und bestimmte Datenwerte daraus errechnen (der zeitintensive Teil). Anschließend erstellt die Funktion selbst noch ein Steuerelement, dass die Datenwerte für den Benutzer darstellt.

    Die Funktion fügt das Steuerelement einer Liste im UI-Thread hinzu. Da da hin und wieder Ausnahmen ausgelöst wurden, habe ich das Hinzufügen in eine unendliche Do...Loop-Schleife gepackt, die Verlassen wird, wenn keine Ausnahme ausgelöst wurde. Ob das als threadsicher zählt, weiß ich leider nicht.

    Der UI-Thread wartet, bis alle Elemente hinzugefügt wurden und fügt sie dann selber der Windows Form hinzu, sodass der Benutzer die Steuerelemente mit den Datenwerten sieht.

    Dieser Teil funktioniert aber per se schon. Nur würde ich gerne während der Teil im Hintergrund abläuft ein Dialogfeld anzeigen, dass eben aussagt, dass und was gerade passiert, und das deswegen nicht eingefroren sein darf.

    Grüße Martin


    Wupp Wupp! Fnak Fnak!

    Mittwoch, 20. März 2019 12:57
  • Hi Martin,
    da gibt es einige Ungereimtheiten:

    Anschließend erstellt die Funktion selbst noch ein Steuerelement

    Ein Steuerelement in einem anderen Thread zu instanziieren bzw. zu nutzen bringt ggf. Fehler, wenn diese Steuerelemente dann im UI-Thread genutzt (hinzugefügt) werden, da sie nicht threadsicher sind.

    Da da hin und wieder Ausnahmen ausgelöst wurden, habe ich das Hinzufügen in eine unendliche Do...Loop-Schleife gepackt

    Welche Fehler? Vermutlich unklar, was auf threadübergreifende Zugriffe zurückzuführen sein kann.

    Do...Loop im IU-Thread blockiert alles (auch die Aktualisierung der Oberfläche). Auf Do...Loop kann man problemlos verzichten, indem man entweder async/await oder auch Ereignissteuerung nutzt.

    Der UI-Thread wartet, bis alle Elemente hinzugefügt wurden und fügt sie dann selber der Windows Form hinzu, sodass der Benutzer die Steuerelemente mit den Datenwerten sieht.

    Do...Loop und Warten sind zwei unterschiedliche Dinge. Do...Loop belastet die CPU zu 100 % (solange darin kein await eingebaut ist). Das scheinbare Warten ist lediglich eine Belastung, die nichts anderes zulässt. Warten bedeutet dagegen, dass die CPU frei ist für andere Programmabläufe, wie beispielsweise die Aktualisierung der Oberfläche.

    Dieser Teil funktioniert aber per se schon.

    Ich meine bei dieser Beschreibung, dass das Funktionieren ein Zufall ist. Ein anderer Rechner, andere parallel laufende Programme und schon funktioniert das u.U. nicht mehr.

    Bau in die asynchronen Methoden lediglich die lang andauernde Berechnung und Arbeit ein, gib am Ende threadsichere Objekte zurück an den UI-Thread und erzeug (instanziiere) im UI-Thread die Steuerelemente, fülle sie mit Daten oder besser noch binde sie an die Daten.


    --
    Viele Grüsse / Best Regards
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks




    Mittwoch, 20. März 2019 14:24
  • Hallo Peter,

    Ein Steuerelement in einem anderen Thread zu instanziieren bzw. zu nutzen bringt ggf. Fehler, wenn diese Steuerelemente dann im UI-Thread genutzt (hinzugefügt) werden, da sie nicht threadsicher sind.

    Dann werde ich wohl die Datenberechnung und Steuerelement-Erstellung trennen müssen.

    Welche Fehler? Vermutlich unklar, was auf threadübergreifende Zugriffe zurückzuführen sein kann.
    Do...Loop im IU-Thread blockiert alles (auch die Aktualisierung der Oberfläche). Auf Do...Loop kann man problemlos verzichten, indem man entweder async/await oder auch Ereignissteuerung nutzt.

    Manchmal kam eine Null-Ausnahme auf, die sich laut Stacktrace auf irgendwas in der Insert-Methode der Liste zurückführen lässt. Daher mein, es einfach solange versuchen, bis es klappt. Die Do...Loop für das Hinzufügen war ja nicht im UI-Thread. Wenn ich auf diese Weise jetzt die resultierenden Datenwerte hinzufüge, muss ich befürchten, dass die Schleife im schlimmsten Fall auch den UI-Thread blockiert, oder bin ich da auf der sicheren Seite?

    Do...Loop und Warten sind zwei unterschiedliche Dinge. Do...Loop belastet die CPU zu 100 % (solange darin kein await eingebaut ist). Das scheinbare Warten ist lediglich eine Belastung, die nichts anderes zulässt. Warten bedeutet dagegen, dass die CPU frei ist für andere Programmabläufe, wie beispielsweise die Aktualisierung der Oberfläche.

    Dass ich das nicht gesehen habe, oje. Lag vermutlich an der späten Uhrzeit. 

    Bau in die asynchronen Methoden lediglich die lang andauernde Berechnung und Arbeit ein, gib am Ende threadsichere Objekte zurück an den UI-Thread und erzeug (instanziiere) im UI-Thread die Steuerelemente, fülle sie mit Daten oder besser noch binde sie an die Daten.

    Das heißt, mittels asynchronen Funktionen berechne ich die gewünschten Daten und gebe die Ergebnisse zurück.

    Meine Idee wäre jetzt, im Hauptthread für jede Funktion einen Task zu erstellen und die Rückgabe zu awaiten und einer Liste hinzuzufügen. Also so (wenn das geht):

    ErgebnisListe.Add(Await BerechnungsTaskX)

    Anschließend, wenn alle Rückgaben awaitet wurden, würde ich wie von Dir vorgeschlagen die Steuerelemente innerhalb des UI-Threads erstellen und anzeigen.

    Hört sich dieser Plan gut an?

    Grüße Martin


    Wupp Wupp! Fnak Fnak!

    Mittwoch, 20. März 2019 15:27
  • Hi Martin,
    dieser Plan erscheint mir nicht optimal. Mit dem awaiter wartest Du genau auf das Ende einer asynchrone ausgeführten Methode. So, wie ich es verstanden hatte, hast Du aber mehrere asynchron abzuarbeitende Methoden. Du kannst aber alle Tasks ohne await starten und dann mit WaitAll auf das Ende aller Tasks warten. Nach den Schilderungen von Dir denke ich aber, dass auch das nicht optimal ist.

    Mir scheint eine Lösung mit einer Ereignissteuerung für Deine Aufgabe am besten geeignet. Für die asynchronen Methoden gibt es Klassen. Wenn alle asynchronen Methoden die gleiche Funktion ausführen (z.B. jeder Aufruf einer asynchronen Methode macht dasselbe aber immer für ein anderes Datenobjekt/Datensatz), dann reicht dazu eine Klasse. Andernfalls könne es auch mehrere Klassen sein, die ggf. von der gleiche Basisklasse erben. Die Klasse hate eine Execute-Methode, die dann einen Thread für die asynchrone Verarbeitung startet. Beim Ende des Threads wird ein Ereignis ausgelöst, welches die Ergebnisdaten liefert oder auch nur signalisiert, dass die Ergebnisdaten in den Eigenschaften des Objektes (der Klasse) bereitstehen.

    Du UI instanziiert für jeden Sachverhalt (z.B. Datenobjekt/Datensatz) ein Objekt, hängt an das Ereignis eine Ereignis-Methode und starte die Execute-Methode die nach dem Start des Threads sofort wieder beendet wird. Die Ereignismethode führt dann (im Context des UI-Threads) die Instanziierung der Steuerelemente, die Bindung der Ergebnisdaten und das Hinzufügen zum Container aus. Da immer auf die Ereignisse gewartet wird, steht auch genügend Zeit für eine Aktualisierung der Oberfläche zur Verfügung.


    --
    Viele Grüsse / Best Regards
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks

    Mittwoch, 20. März 2019 19:35
  • Hallo Peter,

    wenn ich dein Konzept aufgreife, sollte es aber auch möglich sein, nur eine Klasse zu schreiben, die die auszuführende Aufgabe als Variable hat. Die Aufgabe kann durch den UI-Thread beim Instanziieren eines Objekts ausgewiesen werden. Entsprechend so, wie er es auch mit der Ereignis-Methode macht. So müsste ich nur die verschiedenen Aufgaben schreiben und nicht eine Klasse für jede Aufgabe.

    In etwa so:

    Public Class AsyncExecuter Public Property Aufgabe As Func(Of Datensatz) Public Property Ergebnis As Datensatz Public Event AufgabeFertig as EventHandler Public Sub Execute() ' Hier wird die Aufgaben-Eigenschaft asynchron aufgerufen und ' die Rückgabe der Ergebnis-Eigenschaft zugewiesen. ' Anschließend wird das AufgabeFertig-Ereignis ausgelöst. End Sub End Class

    Sollte machbar sein, oder?

    Grüße Martin


    Wupp Wupp! Fnak Fnak!

    Donnerstag, 21. März 2019 13:45
  • Hi Martin,
    nachfolgend mal eine kleine Demo zu Deinem Problem.

    Ich habe mal 2 Execute-Methoden dargestellt, eine mit der Thread-Klasse ohne Begrenzung des Thread-Pools, eine mit BeginInvoke des Delegaten. Wichtig dabei ist, dass das Datensatz-Objekt threadsicher ist. Solange darin nur Eigenschaften genutzt werden, die Wertetypen repräsentieren, ist das auch gewährleistet.

    Bei komplexen Typen in der Klasse Datensatz muss die Thread-Sicherheit geprüft werden. Im Extremfall muss bei fehlender Thread-Sicherheit in der Ereignisroutine eine tiefe Kopie genutzt werden, d.h. es muss im Ausgangs-Thread (UI-Thread) das Datensatz-Objekt instanziiert und mit Werten gefüllt werden. Das könnte man im Aufruf der Ereignisroutine machen. Das bedeutet aber eine Verzögerung, die zu betrachten und ggf. zu minimieren ist.

    Imports System.Runtime.Remoting.Messaging
    Imports System.Threading
    
    Public Class Form07
    
      ' ohne Designer in leerer Form
      Private pan As New FlowLayoutPanel With {.Dock = DockStyle.Fill}
    
      Private Sub Form07_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.Controls.Add(pan)
        ' Verarbeitung nach dem Laden des Forms starten
        StartWork()
      End Sub
    
      Private rnd As New Random ' Zufallszahl für Zeitdauer der Verarbeitung
    
      Private Sub StartWork()
        For i = 1 To 100
          Dim c As New AsyncExecuter ' neue Verarbeitungsklasse erzeugen
          ' Delegate zuweisen
          c.Aufgabe = New Func(Of Integer, Datensatz)(Function(par As Integer) As Datensatz
                                                        ' eine zufällig Verarbeitungsdauer simulieren
                                                        Thread.Sleep(rnd.Next(100, 5000))
                                                        ' das Ergebnis zurückliefern 
                                                        Return New Datensatz With {.ID = par, .Info = $"Data {Thread.CurrentThread.ManagedThreadId}"}
                                                      End Function)
          ' Ende-Ereignis der Verarbeitung behandeln
          AddHandler c.AufgabeFertig, AddressOf C_Fertig
          ' Start der Ausführung
          c.Execute(i)
          ' c.Execute2(i) ' alternative Möglichkeit mit BeginInvoke
        Next
      End Sub
    
      ' Die Ereignismethode wird gleichen Context ausgeführt
      ' damit werden thread-übergreifende Zugriffe unterbunden
      ' Das sichert die Nutzung des SynchronizationContext
      Private Sub C_Fertig(sender As Object, e As EventArgs)
        Dim c = TryCast(sender, AsyncExecuter) ' den Typ des Ereignisauslösers ermitteln
        If c Is Nothing Then Exit Sub ' Sicherheit, falls falsch nicht erwarteter Typ
        ' Steuerelement instanziieren und mit Inhalt füllen
        Dim ctl As New Label With {.Text = $"{c.Ergebnis.ID} - {c.Ergebnis.Info}"}
        ' Steuerelement der Oberfläche hinzufügen
        pan.Controls.Add(ctl)
      End Sub
    
    
      Public Class AsyncExecuter
    
        ' zum Zeitpunkt der Instanziierung wird der aktuelle Context (des Aufrufers) gemerkt,
        ' um spätere Callbacks (Ereignisse) in den Context des Aufrufers zur Abarbeitung einzuordnen
        Private sc As SynchronizationContext = SynchronizationContext.Current
    
        Public Property Aufgabe As Func(Of Integer, Datensatz)
        Public Property Ergebnis As Datensatz
    
        Public Event AufgabeFertig As EventHandler
    
        ' Nutzung der Thread-Klasse
        Public Sub Execute(parameter As Integer)
          ' Hier wird die Aufgaben-Eigenschaft asynchron aufgerufen und
          ' die Rückgabe der Ergebnis-Eigenschaft zugewiesen.
          ' Anschließend wird das AufgabeFertig-Ereignis ausgelöst.
    
          ' Dazu ein neues Thread-Objekt erstellen und starten
          Call (New Thread(New ParameterizedThreadStart(Sub() ' Methode, die im Thread ausgeführt wird
                                                          ' die übergebene Func aufrufen und Ergebnis zuweisen
                                                          Ergebnis = Aufgabe.Invoke(parameter)
                                                          ' im Context des Ausgangsthreads ausführen, um thread-übergreifende Zugriffe auszuschließen
                                                          sc.Post(New SendOrPostCallback(Sub(state As Object)
                                                                                           ' Ereignismethode ausführen
                                                                                           RaiseEvent AufgabeFertig(Me, New EventArgs)
                                                                                         End Sub), parameter)
                                                        End Sub))).Start()
        End Sub
    
        ' Nutzung der asynchronen Verarbeitungsmöglichkeiten des Delagates
        Public Sub Execute2(parameter As Integer)
          Aufgabe.BeginInvoke(parameter, AddressOf Execute2CB, Aufgabe)
        End Sub
    
        Private Sub Execute2CB(ar As IAsyncResult)
          Dim aresult = TryCast(ar, AsyncResult) ' zum Typ AsyncResult casten
          Dim del = TryCast(aresult.AsyncDelegate, Func(Of Integer, Datensatz)) ' den Delegate der Func holen
          Ergebnis = del.EndInvoke(ar) ' das Ergebnis abholen
          ' in den Context des Ausgangsthreads einordnen, um thread-übergreifende Zugriffe auszuschließen
          sc.Post(New SendOrPostCallback(Sub(state As Object)
                                           ' Ereignismethode ausführen
                                           RaiseEvent AufgabeFertig(Me, New EventArgs)
                                         End Sub), Nothing)
        End Sub
      End Class
    
      ' Wichtig ist, dass Objekte dieses Typs (Klasse) threadsicher sind.
      ' Da die Backingfields der Eigenschaften Wertetypen sind, sind auch Objekte
      ' vom Typ dieser Klasse threadsicher.
      Public Class Datensatz
        Public Property ID As Integer
        Public Property Info As String
      End Class
    
    End Class


    --
    Viele Grüsse / Best Regards
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks

    Samstag, 23. März 2019 05:49
  • Hallo Peter,

    also ich habe deinen Code nun entsprechend implementiert und er funktioniert einwandfrei. Die nächsten Tage muss ich mich davor setzen, um zu verstehen, was wofür da ist.

    Danke aber in jedem Fall. Ich wusste nicht, dass Multithreading so rasant so komplex wird. Es wundert mich, dass es für eine solchen Fall "Multithread mit UI Update" keine Lösung bereits im Framework gibt. Also unsere AsyncExecuter-Klasse nur in generell.

    Mithilfe deiner Lösung ist es anschließend auch problemlos gewesen, ein Dialogfeld anzuzeigen, während die Aufgaben im Hintergrund abgearbeitet werden.

    Die StartWork()-Methode und die C_Fertig()-Methode sollte ich ja in eine Klasse packen können, die dann eine Liste an Funktionen in StartWork() anstelle des jetzigen "Thread.Sleep + Return ... " als Aufgaben zuweist.

    Meine Datensatz-Klasse enthält derzeit tatsächlich eine Eigenschaft, die ein Referenztyp ist. Nur wird der Datensatz als Ergebnis ja nur noch vom UI-Thread verwendet und benötigt, d. h. doch, er müsste trotzdem thread-sicher sein, oder?

    Grüße Martin


    Wupp Wupp! Fnak Fnak!

    Montag, 25. März 2019 18:17
  • Hi Martin,
    schön, dass es funktioniert. Ich habe versucht, mit Kommentaren den Code verständlicher zu machen. Wenn es konkrete Fragen gibt, dann stelle sie. Möglicherweise ist das dann auch eine Hilfe für andere, genau wie auch eine Markierung, dass die Antwort hilfreich war.

    Ich wusste nicht, dass Multithreading so rasant so komplex wird.

    Um Multi-Threading zu verstehen, muss man verstehen, wie die Zuteilung der CPU-Ressourcen (Prozessoren) zu den wartenden Prozessen durch das Betriebssystem organisiert ist (Zeitscheiben usw.). Diese Grundkenntnisse werden z.B. in einem mehrjährigen Hochschulstudium vermittelt. Die wenigen Klassen, Methoden, Ereignisse für das Multi-Threading scheinen manche Programmierer zu verleiten zu denken, dass das ganz einfach ist. Es ist aber nicht so. Beim Multi-Threading hat das Betriebssystem Vorrang gegenüber der Laufzeitumgebung. Die Verarbeitung einer Sprachanweisung kann u.U. durch eine Vielzahl von CPU-Befehlen realisiert werden. Eine Folge von CPU-Befehlen kann z.B. durch die Zeitscheibentechnik des Betriebssystems zu einem beliebigen unvorhersehbaren Zeitpunkt unterbrochen werden, um dann einem anderen Prozess (Thread) CPU-Zeit zuzuteilen (Zeitscheibe abgelaufen oder Prozess mit höherer Priorität wurde aktiv gesetzt). Wenn dabei der unterbrochene Thread ein Objekt teilweise verändert hat (z.B. Daten in CPU-Register geladen und verändert) und dabei unterbrochen wird (Speichern der Veränderungen zeitweilig ausgesetzt), dann kann der andere Thread, der die nächste Zeitscheibe bekommen hat, auf ein inkonsistentes Objekt (im Speicher) treffen. Das kann zu Problemen führen. Bei mehreren CPU's in einem System kann das noch viel öfter passieren als beim Unterbrechen der Zeitscheiben, da jeder wartende Thread einfache einer "freien" CPU zur Abarbeitung übergeben wird.

    Es wundert mich, dass es für eine solchen Fall "Multithread mit UI Update" keine Lösung bereits im Framework gibt.

    Natürlich gibt es Lösungen, z.B. die BackgroundWorker-Klasse oder die Task-Klasse mit den Sprachelementen async/await.

    Meine Datensatz-Klasse enthält derzeit tatsächlich eine Eigenschaft, die ein Referenztyp ist. Nur wird der Datensatz als Ergebnis ja nur noch vom UI-Thread verwendet und benötigt, d. h. doch, er müsste trotzdem thread-sicher sein, oder?

    Bei einer Eigenschaft, die auf einen Referenztyp verweist, ist unbedingt zu prüfen, ob das referenzierte Objekt auch threadsicher ist. Die meisten Typen sind beim Lesen threadsicher, nicht aber beim Schreiben. Das kann man sich zunutze machen, indem man eine tiefe Kopie erzeugt, wofür man ja beim Kopiervorgang nur den Lesezugriff benötigt. Außerdem kann man analysieren, was denn wirklich im asynchronen Thread gemacht wird. Meist wird im asynchronen Thread ein Eigenschaftswert des referenzierten Objektes beschrieben. Das Backing-Field dazu ist dann ein Wertetyp. Da passiert noch nichts. Wenn dann jedoch im asynchronen Thread ein Ereignis ausgelöst wird, dann wird die Ereignisroutine im Context dieses Threads ausgeführt, was zu eine thread-übergreifenden Zugriff führen kann. In diesem Fall kann man mit Post (des SynchronizationContext) die Abarbeitung der Ereignisroutinen in den Context des UI-Threads einordnen (vorausgesetzt es wurde der SynchronizationContext des UI-Threads gemerkt). 


    --
    Viele Grüsse / Best Regards
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks

    Montag, 25. März 2019 20:46