Controls Threadsicher machen

    • VB.NET

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

      Controls Threadsicher machen

      Hallo,

      in den folgenden Zeilen möchte ich kurz erklären, wie man für seine eigenen Applicationen Controls Threadsicher und wiederverwendbar macht.

      Jeder weiß, wie er zur Laufzeit der Application einen bestimmten Property Wert ändert, jedoch sobald er sich nicht mehr in dem GUI Thread befindet, wird das ganze schon wieder etwas schwerer und ist mit etwas Gehirnschmalz verbunden.

      z.B. Das Control Label

      VB.NET-Quellcode

      1. Imports System.Threading
      2. Public Class Form1
      3. Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
      4. 'ERFOLGREICH
      5. Me.Label1.Text = "GUI Thread Label"
      6. 'ERFOLGREICH
      7. Me.MyLabel1.Text = "GUI Thread MyLabel"
      8. 'FEHLSCHLAG
      9. Dim newThread1 As New Thread(AddressOf DoWorkLabel)
      10. newThread1.Start()
      11. 'ERFOLGREICH
      12. Dim newThread2 As New Thread(AddressOf DoWorkMyLabel)
      13. newThread2.Start()
      14. End Sub
      15. Private Sub DoWorkLabel()
      16. 'ERROR
      17. 'FEHLER: Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement Label1
      18. 'erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde.
      19. Me.Label1.Text = "Background Thread Label"
      20. End Sub
      21. Private Sub DoWorkMyLabel()
      22. 'ERFOLGREICH
      23. Me.MyLabel1.Text = "Background Thread MyLabel"
      24. End Sub
      25. End Class


      Warum kommt es zum Fehler, wenn ich Me.Label1.Text = "Background Thread Label" aufrufe?
      Das ist recht einfach zu erklären. Me.Label1 gehört zum Haupt-/GUI Thread und ist entsprechend von diesem verwaltet, wenn ich nun über einen Background Thread oder Background Worker diesen Wert ändern will, muss ich dazu natürlich gewisse Richtlinien einhalten. Somit müssen wir eine Möglichkeit schaffen um von einem Nebenthread den Hauptthread anzusprechen und diesem die Änderung mitzuteilen. Damit dieser dann unsere Änderung an seinem verwaltetenen Objekt durchführen kann.



      Wie man in meinem Beispiel sehen kann, verwende ich zum einen das Standard Label Control und zum anderen MyLabel Control. Da MyLabel von Label erbt, besitze ich bereits alle Funktionalitäten, die auch Label bietet, diese kann ich nun mit eigenen Funktionen und Methoden erweitern bzw. überschreiben. Für das oben abgebildete Beispiel bieten sich nun 2 mögliche Wege an wie ich mein Control gestalten kann, zum einen über einen Delegaten und zum anderen über einen Lambda Ausdruck.


      Beispiel Möglichkeit 1 - Sehr viel Schreibaufwand!
      Spoiler anzeigen

      VB.NET-Quellcode

      1. 'Bei dieser Möglichkeit wird der Schreibaufwand stark ansteigen und die Übersichtlichkeit wird darunter stark leiden.
      2. Public Class MyLabel
      3. Inherits Label
      4. 'Wird benötigt, für den Aufruf von diversen Methoden innerhalb des Controls MyLabel
      5. Private Delegate Sub ControlStringInvoke(ByVal value As String)
      6. 'Überschreibt die Eigenschaft von Label, welches hier als Basisklasse fungiert.
      7. Public Overrides Property Text As String
      8. Get
      9. Return MyBase.Text
      10. End Get
      11. Set(ByVal value As String)
      12. 'Benötigen wir einen Invoke, da wir uns in einem Background Thread befinden?
      13. If Me.InvokeRequired Then
      14. Me.Invoke(New ControlStringInvoke(AddressOf SetText), value)
      15. Else
      16. MyBase.Text = value
      17. End If
      18. End Set
      19. End Property
      20. 'Für jede Eigenschaft, die wir Threadsicher machen wollen, benötigen wir eine solche Methode um den Aufruf erneut zu senden.
      21. Private Sub SetText(ByVal value As String)
      22. Me.Text = value
      23. End Sub
      24. End Class


      Möglichkeit 2 - Etwas komplizierter, aber deutlich Pflegeleichter!
      Spoiler anzeigen

      VB.NET-Quellcode

      1. 'Bei dieser Möglichkeit verzichten wir ganz auf Delegates und gewinnen wieder an Übersichtlichkeit, jedoch müssen wir uns mit Lambda Ausdrücken auseinander setzen.
      2. Public Class MyLabel
      3. Inherits Label
      4. 'Überschreibt die Eigenschaft von Label, welches hier als Basisklasse fungiert.
      5. Public Overrides Property Text As String
      6. Get
      7. Return MyBase.Text
      8. End Get
      9. Set(ByVal value As String)
      10. 'Benötigen wir einen Invoke, da wir uns in einem Background Thread befinden?
      11. If Me.InvokeRequired Then
      12. 'Bei diesem Befehl, erhält man von Visual Studio folgende Warnung:
      13. 'Der Ausdruck ruft rekursiv die enthaltene Text-Eigenschaft auf.
      14. 'Was ja im Prinzip richtig ist, sogar gewünscht. :)
      15. Me.Invoke(Sub(x As String) Me.Text = x, value)
      16. Else
      17. MyBase.Text = value
      18. End If
      19. End Set
      20. End Property
      21. End Class


      Ich hoffe ich konnte damit einige Anregungen schaffen.

      Dieser Beitrag wurde bereits 7 mal editiert, zuletzt von „Samtrion“ () aus folgendem Grund: Korrekturen

      [VB.NET] GUIHelper

      Kommt vom Prinzip her dasselbe darin vor, außerdem ist deins ein bisschen umständlich, warum die Variable zu kopieren, wenn du sie eh direkt so angeben kannst?!^^
      Etwas mehr Erklärung, wie z.B. das es sich dabei um Lambda Ausdrücke handelt und warum man diese so verwenden kann und vorallem seit wann wäre nicht schlecht gewesen ;) Ansonsten *ThumbsUp*
      Ich wollte auch mal ne total überflüssige Signatur:
      ---Leer---
      Einige Korrekturen vorgenommen.
      Danke jvvsl, für die Anregungen.

      Doch 2 offene Punkte habe ich noch, dazu müsstest du mir noch etwas mehr Input geben.

      1. Punkt
      Was meinst du mit deinem Satzteil, daraus werde ich aktuell nicht schlau, egal wie ich ihn drehe und wende.

      jvbsl schrieb:

      warum die Variable zu kopieren, wenn du sie eh direkt so angeben kannst?!^^


      2. Punkt
      Sehe ich keinesfalls den GUIHelper als alternative, klar er besitzt einen schönen Ansatz, doch bleibt die Arbeit an dem Programmierer hängen, der ein threadsicheres Projekt umsetzen will.
      Wohin gegen meine Lösung nach Fertigstellung eines kompletten Threadsicheren Control Toolkits (Arbeite ich aktuell dran), die Arbeit bereits erledigt ist.
      1.
      bei deinem Lambda Ausdruck:
      Me.Invoke(Sub(x As String) Me.Text = x, value)

      würde doch so genausogut gehen:

      VB.NET-Quellcode

      1. Me.Invoke(Sub() Me.Text = value)

      bzw. sogar noch kürzer...
      Außerdem ist mir gerade aufgefallen, dass du beim Get ebenfalls noch Invoken müsstest ;)
      2. der GUIHelper hat einmal Arbeit, anschließend ist es sogar kürzer als bei dir(wenn man für alles eine extra Property machen würde)...
      Und Threadsicher ist dieser ebenfalls, arbeitet ja vom Prinzip her mit dem selben Prinzip...
      Und außerdem hab ich das gepostet, da mein dritter Beitrag, eben dies mit den Lambda Ausdrücken anspricht ;)
      Ich wollte auch mal ne total überflüssige Signatur:
      ---Leer---
      gute frage.

      Vlt, weil Invoking sehr viel Performance kostet. Deshalb zwingt man die Programmierer, es explizit anzugeben - damit sie sich was überlegen, dass man sich bei MassenOperationen nicht mit massenhafter Invokerei selbst ausbremst.
      ZB 1000 Zeilen in ein Listview füllen, jede einzeln per Control.Invoke - wäre glaub nicht lustig - müssteman mal testen

      Control.Invoke() vs. Control.BeginInvoke()

      Also Beispiel - so kann man eine Text-Zuweisung threadsicher machen:

      VB.NET-Quellcode

      1. Textbox1.Invoke(Sub()Textbox1.Text = "blabla")

      Aber günstiger ist

      VB.NET-Quellcode

      1. Textbox1.BeginInvoke(Sub()Textbox1.Text = "blabla")


      bei Control.Invoke() wartet der Nebenthread, bis der Mainthread den Delegaten ausgeführt hat. In diesem Fall wird also nicht der Mainthread blockiert, sonnern grad annersrum: Es wird der NebenThread blockiert.
      Was ja üblicherweise nicht Sinn von Nebenläufigkeit ist.

      Control.BeginInvoke() sollte also die Standard-Variante des Invokings sein, und Control.Invoke() nur in besonderen Ausnahmefällen (grob gesagt: ungefähr nie).

      Edit: Ich verlinke hiermal auf einen anneren interessanten Thread, wo allerlei weitere Aspekte des Themas "Threading" behandelt werden.

      Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „ErfinderDesRades“ ()

      how about: der Thread muss auf die Eingabe des Users warten, könnte ja mal sein, dass die dazwischen ist, dann will man einen rückgabewert ;) klar kann man bei BeginInvoke auch bekommen, aber man kann ja sowieso erst weitermachen, wenns fertig ist ;)
      Ich wollte auch mal ne total überflüssige Signatur:
      ---Leer---
      jo, das wäre so eine extrem seltene Ausnahme, nämlich dass der NebenThread weitere Daten von einem Control abrufen muß, um weiterarbeiten zu können.

      Extrem selten ist das, weil meist schlechtes Design: Wenns irgend geht, sollte der Mainthread die Daten bereitstellen, und dann den NebenThread das abarbeiten lassen.

      Selbst wenn das Nachholen weiterer Verarbeitungsdaten wirklich erforderlich sein sollte, wäre es immer noch sehr schlechtes Design, wenn diese Daten direkt aus einem Control abzurufen wären - ein typischer Mangel, wenn Daten und Gui nicht getrennt werden.
      Ordentlich designed würden die nachzuladenden Daten in Datenklassen, etwa Auflistungen bereitgestellt, also der MainThread würde was reinpacken, und der Nebenthread da rausholen. So ein Producer-Consumer-Vorgang wäre nicht mit dem teuren Control.Invoke zu synchronisieren, sondern viel billiger mit Synclock, Monitor, vlt. auch BlockingCollection und Konsorten.

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „ErfinderDesRades“ ()

      ErfinderDesRades schrieb:

      Control.BeginInvoke() sollte also die Standard-Variante des Invokings sein, und Control.Invoke() nur in besonderen Ausnahmefällen (grob gesagt: ungefähr nie).
      Nein.
      Der Vorteil von Control.Invoke ist, daß Du quasi synchron bleibst.
      Mit Control.BeginInvoke musst Du mit Locking-Mechanismen (SyncLock, Monitor, etc. ) ggf. Race-Conditions abfangen.
      ich weiß jetzt grad kein konkretes Beispiel, wo's erforderlich ist, im NebenThread "quasi synchron" zu bleiben.
      Zumal man den Nebenthread ja üblicherweise grade dazu startet, um Code asynchron ausführen zu können.
      Also wenn der Nebenthread weitere Infos einem Control abrufen muß, um weiter arbeiten zu können - das geht natürlich nicht ohne Synchronizität - aber da besteht ja die Frage wg. fragwürdigem Design.

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

      ErfinderDesRades schrieb:

      Ordentlich designed würden die nachzuladenden Daten in Datenklassen, etwa Auflistungen bereitgestellt, also der MainThread würde was reinpacken, und der Nebenthread da rausholen. So ein Producer-Consumer-Vorgang wäre nicht mit dem teuren Control.Invoke zu synchronisieren, sondern viel billiger mit Synclock, Monitor, vlt. auch BlockingCollection und Konsorten.
      Vielleicht sollte ich mit den Antworten warten bis Du Deine Posts nachträglich fertig editiert hast ?

      ErfinderDesRades schrieb:

      ich weiß jetzt grad kein konkretes Beispiel, wo's erforderlich ist, im NebenThread "quasi synchron" zu bleiben.

      Dann lasse ich Dir Zeit darüber nachzudenken. Solltest Du semantische Probleme mit dem Begriff 'quasi synchron' haben , so bin ich gerne bereit das zu verdeutlichen.